/recoverable

Class Level retry DSL for ruby

Primary LanguageRubyMIT LicenseMIT

Version      Build Status Maintainability Test Coverage

Recoverable

Recoverable is a simple DSL that works at the class level to configure retries of instance methods. With a multitude of customizations this gem can combined with ruby's class inheritence can be a powerful tool for drying up code.

Installation

Install the gem:

$ gem install 'recoverable'

Add it to your gemfile:

gem 'recoverable'

Then run bundle to install the Gem:

$ bundle install

Usage

Recoverable gives you a dynamic way to retry and handle errors on an instance of a class or an inherited class.

Default Behavior

You can add recoverable to your class by simply extending the Gem and then telling it which method you would like to recover from and how many times you would like to retry.

  class Foo
    extend Recoverable
    recover :bar, tries: 2

    def bar
      baz
    end

  end

With the above configuration any instance of Foo will recover any StandardError on #bar and retry 2 times without a wait between retries. After the second retry it will raise the error Recoverable::RetryCountExceeded along with the information about what error had occured.

Configuration Options

Recoverable allows for varied configurations to alter the behavior of the rescue and retry.

Errors

Setting up your class with the following will specifically recover on CustomError.

  class Foo
    extend Recoverable
    recover :bar, tries: 2, on: CustomError

    def bar
      baz
    end

  end

Note that this configuration will on rescue and retry on CustomError and will not rescue any other error including StandardError.

Recoverable can rescue on a collection of errors as well, however these must be passed to on: as an array.

  recover :bar, tries: 2, on: [ CustomError, OtherCustomError ]

In the above case both CustomError and OtherCustomError will be rescued on the #bar method.

Wait

Setting up your class with the following configuration will insert a 3 second wait(utilizing ruby Kernel#sleep) between each retry:

  class Foo
    extend Recoverable
    recover :bar, tries: 2, wait: 3

    def bar
      baz
    end

  end

You can override the implementation of wait at the configuration level by including a proc that takes an integer argument:

   recover :bar, tries: 2, wait: 3, wait_method: Proc.new{ |int| "Another implementation utilizing wait time" }

Additionally you can globally override the default wait implementation in an initializer by setting the default wait method:

  Recoverable::Defaults.wait_method = Proc.new|int| 
    #Do something else with wait time here....
  end

Custom Exception

In addition to retrying, recoverable allows you to throw a custom exception after the rescue and retry attempts.

  class MyException < StandardError; end

  class Foo
    extend Recoverable
    recover :bar, tries: 2, throw: MyException

    def bar
      baz
    end

  end

In this configuration after bar was retried twice recoverable would not raise StandardError or Recoverable::RetryCountExceeded but would instead raise MyException

Custom Handler

Recoverable also allows you to configure a custom error handling method. This should be a method defined on the class or parent class of the instance.

  class Foo
    extend Recoverable
    recover :bar, tries: 2, custom_handler: :handle_error

    def bar
      baz
    end

    def handle_error(error:)
      "#{error} was retried twice, raised and then this method was called."
    end
  end

Please note that the name of the handler method should be passed to the configuration as a symbol. Also, the handler method can take either no arguments or a single keyword argument for error: if you would like access to the error inside the handler. Any other data inside the handler should be retrieved via instance methods or instance variables.

Inheritence

One of the more powerful aspects of the recoverable implementation is how it handles inheritence.

In the following example, recoverable is setup on the #bar method which is defined on both the parent and child class.

  class ParentClass
    extend Recoverable
    recover :bar, tries: 2
    def bar
      baz
    end

  end

  class ChildClass < ParentClass
    def bar
      super
    end

    def baz; end
  end

Now any call to bar that results in an error registered with the recoverable gem will be rescued and retried based on the configuration as long as the error occurs in the parent scope.

However in the following case the recoverable gem will rescue the error at the child level even though the error occurs in the parent class:

  class ParentClass
    def bar
      baz
    end

  end

  class ChildClass < ParentClass
    extend Recoverable
    recover :bar, tries: 2
    def bar
      super
    end

    def baz; end
  end

The gem will rescue down through multiple inheritence as well:

  class ParentClass
    extend Recoverable
    recover :bar, tries: 2
    def bar
      baz
    end

  end

  class ChildClass < ParentClass
    def baz; end
  end

  class SubChildClass < ChildClass
    def bar
      super
    end
  end

In the above, a call to the subchild class will throw an error that will be caught and retried by the recoverable configuration in the top level parent class

Lastly, error handler methods can be defined on either the parent or child class. For example, assuming that the method bar is called from a ChildClass instance, in the first example below the handle_error method will be called from the ParentClass

 class ParentClass
    extend Recoverable
    recover :bar, tries: 2, custom_handler: :handle_error

    def bar
      baz
    end

    def handle_error(error:)
      "Parent Handler!"
    end
  end

  class ChildClass < ParentClass
    def baz; end
  end

However, in the next example, the same configuration would call the handle_error method from the ChildClass instance.

class ParentClass
  extend Recoverable
  recover :bar, tries: 2, custom_handler: :handle_error

  def bar
    baz
  end

  def handle_error(error:)
    "Parent Handler!"
  end

end

class ChildClass < ParentClass
  def baz; end

  def handle_error(error:)
    "Child Handler!"
  end
end

How to contribute

  • Fork the project
  • Create your feature or bug fix
  • Add the requried tests for it.
  • Commit (do not change version or history)
  • Send a pull request against the development branch

Copyright

Copyright (c) 2018 Ben Jacobs Licenced under the MIT licence.