diff --git a/README.md b/README.md index a941ce4..0e3ea66 100644 --- a/README.md +++ b/README.md @@ -203,11 +203,12 @@ In addition to the classic circuit breaker design, Faulty implements caching that is integrated with the circuit state. See [Caching](#caching) for more detail. -## Global Configuration +## Configuration -`Faulty.init` can set the following global configuration options. This example -illustrates the default values. It is also possible to define multiple -non-global configuration scopes (see [Scopes](#scopes)). +Faulty can be configured with the following configuration options. This example +illustrates the default values. In the first example, we configure Faulty +globally. The second example shows the same configuration using an instance of +Faulty instead of global configuration. ```ruby Faulty.init do |config| @@ -233,6 +234,25 @@ Faulty.init do |config| end ``` +Here is the same configuration using an instance of `Faulty`. This is a more +object-oriented approach. + +```ruby +faulty = Faulty.new do |config| + config.cache = Faulty::Cache::Default.new + config.storage = Faulty::Storage::Memory.new + config.listeners = [Faulty::Events::LogListener.new] + config.notifier = Faulty::Events::Notifier.new(config.listeners) +end +``` + +Most of the examples in this README use the global Faulty class methods, but +they work the same way when using an instance. Just substitute your instance +instead of `Faulty`. There is no preferred way to use Faulty. Choose whichever +configuration mechanism works best for your application. Also see +[Multiple Configurations](#multiple-configurations) if your application needs +to set different options in different scenarios. + For all Faulty APIs that have configuration, you can also pass in an options hash. For example, `Faulty.init` could be called like this: @@ -244,9 +264,9 @@ Faulty.init(cache: Faulty::Cache::Null.new) A circuit can be created with the following configuration options. Those options are only set once, synchronized across threads, and will persist in-memory until -the process exits. If you're using [scopes](#scopes), the options are retained -within the context of each scope. All options given after the first call to -`Faulty.circuit` (or `Scope.circuit`) are ignored. +the process exits. If you're using [multiple configurations](#multiple-configurations), +the options are retained within the context of each instance. All options given +after the first call to `Faulty.circuit` (or `Faulty#circuit`) are ignored. This is because the circuit objects themselves are internally memoized, and are read-only once created. @@ -307,8 +327,8 @@ Faulty.circuit(:api, cache_expires_in: 1800) Faulty integrates caching into it's circuits in a way that is particularly suited to fault-tolerance. To make use of caching, you must specify the `cache` -configuration option when initializing Faulty or creating a scope. If you're -using Rails, this is automatically set to the Rails cache. +configuration option when initializing Faulty or creating a new Faulty instance. +If you're using Rails, this is automatically set to the Rails cache. Once your cache is configured, you can use the `cache` parameter when running a circuit to specify a cache key: @@ -480,8 +500,8 @@ end ## Listing Circuits For monitoring or debugging, you may need to retrieve a list of all circuit -names. This is possible with `Faulty.list_circuits` (or the equivalent method on -your [scope](#scopes)). +names. This is possible with `Faulty.list_circuits` (or `Faulty#list_circuits` +if you're using an instance). You can get a list of all circuit statuses by mapping those names to their status objects. Be careful though, since this could cause performance issues for @@ -523,73 +543,73 @@ Locking or unlocking a circuit has no concurrency guarantees, so it's not recommended to lock or unlock circuits from production code. Instead, locks are intended as an emergency tool for troubleshooting and debugging. -## Scopes +## Multiple Configurations It is possible to have multiple configurations of Faulty running within the same -process. The most common configuration is to simply use `Faulty.init` to +process. The most common setup is to simply use `Faulty.init` to configure Faulty globally, however it is possible to have additional -configurations using scopes. +configurations. -### The default scope +### The default instance -When you call `Faulty.init`, you are actually creating the default scope. You -can access this scope directly by calling `Faulty.default`. +When you call `Faulty.init`, you are actually creating the default instance of +`Faulty`. You can access this instance directly by calling `Faulty.default`. ```ruby -# We create the default scope +# We create the default instance Faulty.init -# Access the default scope -scope = Faulty.default +# Access the default instance +faulty = Faulty.default -# Alternatively, access the scope by name -scope = Faulty[:default] +# Alternatively, access the instance by name +faulty = Faulty[:default] ``` -You can rename the default scope if desired: +You can rename the default instance if desired: ```ruby Faulty.init(:custom_default) -scope = Faulty.default -scope = Faulty[:custom_default] +instance = Faulty.default +instance = Faulty[:custom_default] ``` -### Multiple Scopes +### Multiple Instances -If you want multiple scopes, but want global, thread-safe access to +If you want multiple instance, but want global, thread-safe access to them, you can use `Faulty.register`: ```ruby -api_scope = Faulty::Scope.new do |config| +api_faulty = Faulty.new do |config| # This accepts the same options as Faulty.init end -Faulty.register(:api, api_scope) +Faulty.register(:api, api_faulty) -# Now access the scope globally +# Now access the instance globally Faulty[:api] ``` When you call `Faulty.circuit`, that's the same as calling -`Faulty.default.circuit`, so you can apply the same API to any other Faulty -scope: +`Faulty.default.circuit`, so you can apply the same principal to any other +registered Faulty instance: ```ruby Faulty[:api].circuit(:api_circuit).run { 'ok' } ``` -### Standalone Scopes +### Standalone Instances -If you choose, you can use Faulty scopes without registering them globally. This -could be useful if you prefer dependency injection over global state. +If you choose, you can use Faulty instances without registering them globally. +This is more object-oriented and is necessary if you use dependency injection. ```ruby -faulty = Faulty::Scope.new +faulty = Faulty.new faulty.circuit(:standalone_circuit) ``` -Calling `circuit` on the scope still has the same memoization behavior that +Calling `#circuit` on the instance still has the same memoization behavior that `Faulty.circuit` has, so subsequent calls to the same circuit will return a memoized circuit object. @@ -660,7 +680,7 @@ but there are and have been many other options: - Simple API but configurable for advanced users - Pluggable storage backends (circuitbox also has this) -- Global, or local configuration with scopes +- Global, or object-oriented configuration with multiple instances - Integrated caching support tailored for fault-tolerance - Manually lock circuits open or closed diff --git a/lib/faulty.rb b/lib/faulty.rb index 83bb972..8db9b2a 100644 --- a/lib/faulty.rb +++ b/lib/faulty.rb @@ -9,14 +9,16 @@ require 'faulty/error' require 'faulty/events' require 'faulty/result' -require 'faulty/scope' require 'faulty/status' require 'faulty/storage' -# The top-level namespace for Faulty +# The {Faulty} class has class-level methods for global state or can be +# instantiated to create an independent configuration. # -# Fault-tolerance tools for ruby based on circuit-breakers -module Faulty +# If you are using global state, call {Faulty#init} during your application's +# initialization. This is the simplest way to use {Faulty}. If you prefer, you +# can also call {Faulty.new} to create independent {Faulty} instances. +class Faulty class << self # Start the Faulty environment # @@ -27,78 +29,79 @@ class << self # are spawned. # # If you prefer dependency-injection instead of global state, you can skip - # init and pass a {Scope} directly to your dependencies. + # `init` and use {Faulty.new} to pass an instance directoy to your + # dependencies. # - # @param scope_name [Symbol] The name of the default scope. Can be set to - # `nil` to skip creating a default scope. - # @param config [Hash] Attributes for {Scope::Options} - # @yield [Scope::Options] For setting options in a block + # @param default_name [Symbol] The name of the default instance. Can be set + # to `nil` to skip creating a default instance. + # @param config [Hash] Attributes for {Faulty::Options} + # @yield [Faulty::Options] For setting options in a block # @return [self] - def init(scope_name = :default, **config, &block) - raise AlreadyInitializedError if @scopes + def init(default_name = :default, **config, &block) + raise AlreadyInitializedError if @instances - @default_scope = scope_name - @scopes = Concurrent::Map.new - register(scope_name, Scope.new(**config, &block)) unless scope_name.nil? + @default_instance = default_name + @instances = Concurrent::Map.new + register(default_name, new(**config, &block)) unless default_name.nil? self rescue StandardError - @scopes = nil + @instances = nil raise end - # Get the default scope given during {.init} + # Get the default instance given during {.init} # - # @return [Scope, nil] The default scope if it is registered + # @return [Faulty, nil] The default instance if it is registered def default - raise UninitializedError unless @scopes - raise MissingDefaultScopeError unless @default_scope + raise UninitializedError unless @instances + raise MissingDefaultInstanceError unless @default_instance - self[@default_scope] + self[@default_instance] end - # Get a scope by name + # Get an instance by name # - # @return [Scope, nil] The named scope if it is registered - def [](scope_name) - raise UninitializedError unless @scopes + # @return [Faulty, nil] The named instance if it is registered + def [](name) + raise UninitializedError unless @instances - @scopes[scope_name] + @instances[name] end - # Register a scope to the global Faulty state + # Register an instance to the global Faulty state # - # Will not replace an existing scope with the same name. Check the - # return value if you need to know whether the scope already existed. + # Will not replace an existing instance with the same name. Check the + # return value if you need to know whether the instance already existed. # - # @param name [Symbol] The name of the scope to register - # @param scope [Scope] The scope to register - # @return [Scope, nil] The previously-registered scope of that name if + # @param name [Symbol] The name of the instance to register + # @param instance [Faulty] The instance to register + # @return [Faulty, nil] The previously-registered instance of that name if # it already existed, otherwise nil. - def register(name, scope) - raise UninitializedError unless @scopes + def register(name, instance) + raise UninitializedError unless @instances - @scopes.put_if_absent(name, scope) + @instances.put_if_absent(name, instance) end - # Get the options for the default scope + # Get the options for the default instance # - # @raise MissingDefaultScopeError If the default scope has not been created - # @return [Scope::Options] + # @raise MissingDefaultInstanceError If the default instance has not been created + # @return [Faulty::Options] def options default.options end - # Get or create a circuit for the default scope + # Get or create a circuit for the default instance # - # @raise UninitializedError If the default scope has not been created - # @param (see Scope#circuit) - # @yield (see Scope#circuit) - # @return (see Scope#circuit) + # @raise UninitializedError If the default instance has not been created + # @param (see Faulty#circuit) + # @yield (see Faulty#circuit) + # @return (see Faulty#circuit) def circuit(name, **config, &block) default.circuit(name, **config, &block) end - # Get a list of all circuit names for the default scope + # Get a list of all circuit names for the default instance # # @return [Array] The circuit names def list_circuits @@ -115,4 +118,113 @@ def current_time Time.now.to_i end end + + attr_reader :options + + # Options for {Faulty} + # + # @!attribute [r] cache + # @return [Cache::Interface] A cache backend if you want + # to use Faulty's cache support. Automatically wrapped in a + # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`. + # @!attribute [r] storage + # @return [Storage::Interface] The storage backend. + # Automatically wrapped in a {Storage::FaultTolerantProxy}. + # Default `Storage::Memory.new`. + # @!attribute [r] listeners + # @return [Array] listeners Faulty event listeners + # @!attribute [r] notifier + # @return [Events::Notifier] A Faulty notifier. If given, listeners are + # ignored. + Options = Struct.new( + :cache, + :storage, + :listeners, + :notifier + ) do + include ImmutableOptions + + private + + def finalize + self.notifier ||= Events::Notifier.new(listeners || []) + + self.storage ||= Storage::Memory.new + unless storage.fault_tolerant? + self.storage = Storage::FaultTolerantProxy.new(storage, notifier: notifier) + end + + self.cache ||= Cache::Default.new + unless cache.fault_tolerant? + self.cache = Cache::FaultTolerantProxy.new(cache, notifier: notifier) + end + end + + def required + %i[cache storage notifier] + end + + def defaults + { + listeners: [Events::LogListener.new] + } + end + end + + # Create a new {Faulty} instance + # + # Note, the process of creating a new instance is not thread safe, + # so make sure instances are setup during your application's initialization + # phase. + # + # For the most part, {Faulty} instances are independent, however for some + # cache and storage backends, you will need to ensure that the cache keys + # and circuit names don't overlap between instances. For example, if using the + # {Storage::Redis} storage backend, you should specify different key + # prefixes for each instance. + # + # @see Options + # @param options [Hash] Attributes for {Options} + # @yield [Options] For setting options in a block + def initialize(**options, &block) + @circuits = Concurrent::Map.new + @options = Options.new(options, &block) + end + + # Create or retrieve a circuit + # + # Within an instance, circuit instances have unique names, so if the given circuit + # name already exists, then the existing circuit will be returned, otherwise + # a new circuit will be created. If an existing circuit is returned, then + # the {options} param and block are ignored. + # + # @param name [String] The name of the circuit + # @param options [Hash] Attributes for {Circuit::Options} + # @yield [Circuit::Options] For setting options in a block + # @return [Circuit] The new circuit or the existing circuit if it already exists + def circuit(name, **options, &block) + name = name.to_s + options = options.merge(circuit_options) + @circuits.compute_if_absent(name) do + Circuit.new(name, **options, &block) + end + end + + # Get a list of all circuit names + # + # @return [Array] The circuit names + def list_circuits + options.storage.list + end + + private + + # Get circuit options from the {Faulty} options + # + # @return [Hash] The circuit options + def circuit_options + options = @options.to_h + options.delete(:listeners) + options + end end diff --git a/lib/faulty/cache.rb b/lib/faulty/cache.rb index 930ee76..6ea7909 100644 --- a/lib/faulty/cache.rb +++ b/lib/faulty/cache.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # The namespace for Faulty caching module Cache end diff --git a/lib/faulty/cache/default.rb b/lib/faulty/cache/default.rb index 2fd8090..b045240 100644 --- a/lib/faulty/cache/default.rb +++ b/lib/faulty/cache/default.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Cache # The default cache implementation # diff --git a/lib/faulty/cache/fault_tolerant_proxy.rb b/lib/faulty/cache/fault_tolerant_proxy.rb index 5d8f388..4a09e68 100644 --- a/lib/faulty/cache/fault_tolerant_proxy.rb +++ b/lib/faulty/cache/fault_tolerant_proxy.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -module Faulty +class Faulty module Cache # A wrapper for cache backends that may raise errors # - # {Scope} automatically wraps all non-fault-tolerant cache backends with + # {Faulty#initialize} automatically wraps all non-fault-tolerant cache backends with # this class. # # If the cache backend raises a `StandardError`, it will be captured and diff --git a/lib/faulty/cache/interface.rb b/lib/faulty/cache/interface.rb index 7b2e227..7c6a3fe 100644 --- a/lib/faulty/cache/interface.rb +++ b/lib/faulty/cache/interface.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Cache # The interface required for a cache backend implementation # diff --git a/lib/faulty/cache/mock.rb b/lib/faulty/cache/mock.rb index 8d1b9fe..430867f 100644 --- a/lib/faulty/cache/mock.rb +++ b/lib/faulty/cache/mock.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Cache # A mock cache for testing # diff --git a/lib/faulty/cache/null.rb b/lib/faulty/cache/null.rb index 0437aed..f64d4e5 100644 --- a/lib/faulty/cache/null.rb +++ b/lib/faulty/cache/null.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Cache # A cache backend that does nothing # diff --git a/lib/faulty/cache/rails.rb b/lib/faulty/cache/rails.rb index eed3238..df87b7c 100644 --- a/lib/faulty/cache/rails.rb +++ b/lib/faulty/cache/rails.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Cache # A wrapper for a Rails or ActiveSupport cache # diff --git a/lib/faulty/circuit.rb b/lib/faulty/circuit.rb index c0647d9..3d57f0f 100644 --- a/lib/faulty/circuit.rb +++ b/lib/faulty/circuit.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # Runs code protected by a circuit breaker # # https://www.martinfowler.com/bliki/CircuitBreaker.html diff --git a/lib/faulty/error.rb b/lib/faulty/error.rb index 40ca67e..d191a16 100644 --- a/lib/faulty/error.rb +++ b/lib/faulty/error.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # The base error for all Faulty errors class FaultyError < StandardError; end @@ -20,10 +20,10 @@ def initialize(message = nil) end end - # Raised if getting the default scope without initializing one - class MissingDefaultScopeError < FaultyError + # Raised if getting the default instance without initializing one + class MissingDefaultInstanceError < FaultyError def initialize(message = nil) - message ||= 'No default scope. Create one with init or get your scope with Faulty[:scope_name]' + message ||= 'No default instance. Create one with init or get your instance with Faulty[:name]' super(message) end end diff --git a/lib/faulty/events.rb b/lib/faulty/events.rb index 0639e34..6a5e532 100644 --- a/lib/faulty/events.rb +++ b/lib/faulty/events.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # The namespace for Faulty events and event listeners module Events # All possible events that can be raised by Faulty diff --git a/lib/faulty/events/callback_listener.rb b/lib/faulty/events/callback_listener.rb index d2bb3e2..5711e47 100644 --- a/lib/faulty/events/callback_listener.rb +++ b/lib/faulty/events/callback_listener.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Events # A simple listener implementation that uses callback blocks as handlers # diff --git a/lib/faulty/events/honeybadger_listener.rb b/lib/faulty/events/honeybadger_listener.rb index 6dc290c..bca66b6 100644 --- a/lib/faulty/events/honeybadger_listener.rb +++ b/lib/faulty/events/honeybadger_listener.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Events # Reports circuit errors to Honeybadger # diff --git a/lib/faulty/events/listener_interface.rb b/lib/faulty/events/listener_interface.rb index b49741e..4fd815e 100644 --- a/lib/faulty/events/listener_interface.rb +++ b/lib/faulty/events/listener_interface.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Events # The interface required to implement a event listener # diff --git a/lib/faulty/events/log_listener.rb b/lib/faulty/events/log_listener.rb index 55f48b1..c30c4fc 100644 --- a/lib/faulty/events/log_listener.rb +++ b/lib/faulty/events/log_listener.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Events # A default listener that logs Faulty events class LogListener diff --git a/lib/faulty/events/notifier.rb b/lib/faulty/events/notifier.rb index a97e0d3..2f1e57d 100644 --- a/lib/faulty/events/notifier.rb +++ b/lib/faulty/events/notifier.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Events # The default event dispatcher for Faulty class Notifier diff --git a/lib/faulty/immutable_options.rb b/lib/faulty/immutable_options.rb index 96da1bb..ce4d23d 100644 --- a/lib/faulty/immutable_options.rb +++ b/lib/faulty/immutable_options.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # A struct that cannot be modified after initialization module ImmutableOptions # @param hash [Hash] A hash of attributes to initialize with diff --git a/lib/faulty/result.rb b/lib/faulty/result.rb index c4e1c94..529cc56 100644 --- a/lib/faulty/result.rb +++ b/lib/faulty/result.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # An approximation of the `Result` type from some strongly-typed languages. # # F#: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/results diff --git a/lib/faulty/scope.rb b/lib/faulty/scope.rb deleted file mode 100644 index 703a69a..0000000 --- a/lib/faulty/scope.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -module Faulty - # A {Scope} is a group of options and circuits - # - # For most use-cases the default scope should be used, however, it's possible - # to create any number of scopes for applications that require a more complex - # configuration or for testing. - # - # For the most part, scopes are independent, however for some cache and - # storage backends, you will need to ensure that the cache keys and circuit - # names don't overlap between scopes. For example, if using the Redis storage - # backend, you should specify different key prefixes for each scope. - class Scope - attr_reader :options - - # Options for {Scope} - # - # @!attribute [r] cache - # @return [Cache::Interface] A cache backend if you want - # to use Faulty's cache support. Automatically wrapped in a - # {Cache::FaultTolerantProxy}. Default `Cache::Default.new`. - # @!attribute [r] storage - # @return [Storage::Interface] The storage backend. - # Automatically wrapped in a {Storage::FaultTolerantProxy}. - # Default `Storage::Memory.new`. - # @!attribute [r] listeners - # @return [Array] listeners Faulty event listeners - # @!attribute [r] notifier - # @return [Events::Notifier] A Faulty notifier. If given, listeners are - # ignored. - Options = Struct.new( - :cache, - :storage, - :listeners, - :notifier - ) do - include ImmutableOptions - - private - - def finalize - self.notifier ||= Events::Notifier.new(listeners || []) - - self.storage ||= Storage::Memory.new - unless storage.fault_tolerant? - self.storage = Storage::FaultTolerantProxy.new(storage, notifier: notifier) - end - - self.cache ||= Cache::Default.new - unless cache.fault_tolerant? - self.cache = Cache::FaultTolerantProxy.new(cache, notifier: notifier) - end - end - - def required - %i[cache storage notifier] - end - - def defaults - { - listeners: [Events::LogListener.new] - } - end - end - - # Create a new Faulty Scope - # - # Note, the process of creating a new scope is not thread safe, - # so make sure scopes are setup before spawning threads. - # - # @see Options - # @param options [Hash] Attributes for {Options} - # @yield [Options] For setting options in a block - def initialize(**options, &block) - @circuits = Concurrent::Map.new - @options = Options.new(options, &block) - end - - # Create or retrieve a circuit - # - # Within a scope, circuit instances have unique names, so if the given circuit - # name already exists, then the existing circuit will be returned, otherwise - # a new circuit will be created. If an existing circuit is returned, then - # the {options} param and block are ignored. - # - # @param name [String] The name of the circuit - # @param options [Hash] Attributes for {Circuit::Options} - # @yield [Circuit::Options] For setting options in a block - # @return [Circuit] The new circuit or the existing circuit if it already exists - def circuit(name, **options, &block) - name = name.to_s - options = options.merge(circuit_options) - @circuits.compute_if_absent(name) do - Circuit.new(name, **options, &block) - end - end - - # Get a list of all circuit names - # - # @return [Array] The circuit names - def list_circuits - options.storage.list - end - - private - - # Get circuit options from the scope options - # - # @return [Hash] The circuit options - def circuit_options - options = @options.to_h - options.delete(:listeners) - options - end - end -end diff --git a/lib/faulty/status.rb b/lib/faulty/status.rb index 9b1696d..485411d 100644 --- a/lib/faulty/status.rb +++ b/lib/faulty/status.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # The status of a circuit # # Includes information like the state and locks. Also calculates diff --git a/lib/faulty/storage.rb b/lib/faulty/storage.rb index 25825c3..bc0f05b 100644 --- a/lib/faulty/storage.rb +++ b/lib/faulty/storage.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # The namespace for Faulty storage module Storage end diff --git a/lib/faulty/storage/fault_tolerant_proxy.rb b/lib/faulty/storage/fault_tolerant_proxy.rb index 1193050..f03402d 100644 --- a/lib/faulty/storage/fault_tolerant_proxy.rb +++ b/lib/faulty/storage/fault_tolerant_proxy.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -module Faulty +class Faulty module Storage # A wrapper for storage backends that may raise errors # - # {Scope} automatically wraps all non-fault-tolerant storage backends with + # {Faulty#initialize} automatically wraps all non-fault-tolerant storage backends with # this class. # # If the storage backend raises a `StandardError`, it will be captured and diff --git a/lib/faulty/storage/interface.rb b/lib/faulty/storage/interface.rb index 9336489..7dd6e5b 100644 --- a/lib/faulty/storage/interface.rb +++ b/lib/faulty/storage/interface.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Storage # The interface required for a storage backend implementation # diff --git a/lib/faulty/storage/memory.rb b/lib/faulty/storage/memory.rb index 65798d7..64b6ee9 100644 --- a/lib/faulty/storage/memory.rb +++ b/lib/faulty/storage/memory.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Storage # The default in-memory storage for circuits # diff --git a/lib/faulty/storage/redis.rb b/lib/faulty/storage/redis.rb index 07b0495..4c3a43a 100644 --- a/lib/faulty/storage/redis.rb +++ b/lib/faulty/storage/redis.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Storage class Redis # rubocop:disable Metrics/ClassLength # Separates the time/status for history entry strings diff --git a/lib/faulty/version.rb b/lib/faulty/version.rb index d0248ad..5339f8d 100644 --- a/lib/faulty/version.rb +++ b/lib/faulty/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty # The current Faulty version def self.version Gem::Version.new('0.1.5') diff --git a/spec/faulty_spec.rb b/spec/faulty_spec.rb index b49cebb..70dee9c 100644 --- a/spec/faulty_spec.rb +++ b/spec/faulty_spec.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true RSpec.describe Faulty do + subject(:instance) { described_class.new(listeners: []) } + after do # Reset the global Faulty instance # We don't want to expose a public method to do this # because it could cause concurrency errors, and confusion about what # exactly gets reset - described_class.instance_variable_set(:@scopes, nil) - described_class.instance_variable_set(:@default_scope, nil) + described_class.instance_variable_set(:@instances, nil) + described_class.instance_variable_set(:@default_instance, nil) end it 'can be initialized with no args' do described_class.init - expect(described_class.default).to be_a(Faulty::Scope) + expect(described_class.default).to be_a(described_class) end - it 'gets options from the default scope' do + it 'gets options from the default instance' do described_class.init expect(described_class.options).to eq(described_class.default.options) end @@ -24,14 +26,14 @@ expect { described_class.default }.to raise_error(Faulty::UninitializedError) end - it '#default raises missing scope error if default not created' do + it '#default raises missing instance error if default not created' do described_class.init(nil) - expect { described_class.default }.to raise_error(Faulty::MissingDefaultScopeError) + expect { described_class.default }.to raise_error(Faulty::MissingDefaultInstanceError) end - it 'can rename the default scope on #init' do + it 'can rename the default instance on #init' do described_class.init(:foo) - expect(described_class.default).to be_a(Faulty::Scope) + expect(described_class.default).to be_a(described_class) expect(described_class[:foo]).to eq(described_class.default) end @@ -40,36 +42,36 @@ described_class.init end - it 'registers a named scope' do + it 'registers a named instance' do described_class.init - scope = Faulty::Scope.new - described_class.register(:new_scope, scope) - expect(described_class[:new_scope]).to eq(scope) + instance = described_class.new + described_class.register(:new_instance, instance) + expect(described_class[:new_instance]).to eq(instance) end - it 'registers a named scope without default' do + it 'registers a named instance without default' do described_class.init(nil) - scope = Faulty::Scope.new - described_class.register(:new_scope, scope) - expect(described_class[:new_scope]).to eq(scope) + instance = described_class.new + described_class.register(:new_instance, instance) + expect(described_class[:new_instance]).to eq(instance) end - it 'memoizes named scopes' do + it 'memoizes named instances' do described_class.init - scope1 = Faulty::Scope.new - scope2 = Faulty::Scope.new - expect(described_class.register(:named, scope1)).to eq(nil) - expect(described_class.register(:named, scope2)).to eq(scope1) - expect(described_class[:named]).to eq(scope1) + instance1 = described_class.new + instance2 = described_class.new + expect(described_class.register(:named, instance1)).to eq(nil) + expect(described_class.register(:named, instance2)).to eq(instance1) + expect(described_class[:named]).to eq(instance1) end - it 'delegates circuit to the default scope' do + it 'delegates circuit to the default instance' do described_class.init(listeners: []) described_class.circuit('test').run { 'ok' } expect(described_class.default.list_circuits).to eq(['test']) end - it 'lists the circuits from the default scope' do + it 'lists the circuits from the default instance' do described_class.init(listeners: []) described_class.circuit('test').run { 'ok' } expect(described_class.list_circuits).to eq(['test']) @@ -79,4 +81,45 @@ Timecop.freeze(Time.new(2020, 1, 1, 0, 0, 0, '+00:00')) expect(described_class.current_time).to eq(1_577_836_800) end + + it 'memoizes circuits' do + expect(instance.circuit('test')).to eq(instance.circuit('test')) + end + + it 'keeps options passed to the first instance and ignores others' do + instance.circuit('test', cool_down: 404) + expect(instance.circuit('test', cool_down: 302).options.cool_down).to eq(404) + end + + it 'converts symbol names to strings' do + expect(instance.circuit(:test)).to eq(instance.circuit('test')) + end + + it 'lists circuit names' do + instance.circuit('test1').run { 'ok' } + instance.circuit('test2').run { 'ok' } + expect(instance.list_circuits).to match_array(%w[test1 test2]) + end + + it 'does not wrap fault-tolerant storage' do + storage = Faulty::Storage::Memory.new + instance = described_class.new(storage: storage) + expect(instance.options.storage).to equal(storage) + end + + it 'does not wrap fault-tolerant cache' do + cache = Faulty::Cache::Null.new + instance = described_class.new(cache: cache) + expect(instance.options.cache).to equal(cache) + end + + it 'wraps non-fault-tolerant storage in FaultTolerantProxy' do + instance = described_class.new(storage: Faulty::Storage::Redis.new) + expect(instance.options.storage).to be_a(Faulty::Storage::FaultTolerantProxy) + end + + it 'wraps non-fault-tolerant cache in FaultTolerantProxy' do + instance = described_class.new(cache: Faulty::Cache::Rails.new(nil)) + expect(instance.options.cache).to be_a(Faulty::Cache::FaultTolerantProxy) + end end diff --git a/spec/scope_spec.rb b/spec/scope_spec.rb deleted file mode 100644 index 49af371..0000000 --- a/spec/scope_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faulty::Scope do - subject(:scope) { described_class.new(listeners: []) } - - it 'memoizes circuits' do - expect(scope.circuit('test')).to eq(scope.circuit('test')) - end - - it 'keeps options passed to the first instance and ignores others' do - scope.circuit('test', cool_down: 404) - expect(scope.circuit('test', cool_down: 302).options.cool_down).to eq(404) - end - - it 'converts symbol names to strings' do - expect(scope.circuit(:test)).to eq(scope.circuit('test')) - end - - it 'lists circuit names' do - scope.circuit('test1').run { 'ok' } - scope.circuit('test2').run { 'ok' } - expect(scope.list_circuits).to match_array(%w[test1 test2]) - end - - it 'does not wrap fault-tolerant storage' do - storage = Faulty::Storage::Memory.new - scope = described_class.new(storage: storage) - expect(scope.options.storage).to equal(storage) - end - - it 'does not wrap fault-tolerant cache' do - cache = Faulty::Cache::Null.new - scope = described_class.new(cache: cache) - expect(scope.options.cache).to equal(cache) - end - - it 'wraps non-fault-tolerant storage in FaultTolerantProxy' do - scope = described_class.new(storage: Faulty::Storage::Redis.new) - expect(scope.options.storage).to be_a(Faulty::Storage::FaultTolerantProxy) - end - - it 'wraps non-fault-tolerant cache in FaultTolerantProxy' do - scope = described_class.new(cache: Faulty::Cache::Rails.new(nil)) - expect(scope.options.cache).to be_a(Faulty::Cache::FaultTolerantProxy) - end -end diff --git a/spec/support/concurrency.rb b/spec/support/concurrency.rb index 1f9a458..8cf3a51 100644 --- a/spec/support/concurrency.rb +++ b/spec/support/concurrency.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Faulty +class Faulty module Specs module Concurrency def concurrent_warmup(&block)