[![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](http://cultofmartians.com/tasks/isolator.html) [![Gem Version](https://badge.fury.io/rb/isolator.svg)](https://badge.fury.io/rb/isolator) [![Build Status](https://travis-ci.org/palkan/isolator.svg?branch=master)](https://travis-ci.org/palkan/isolator) # Isolator Detect non-atomic interactions within DB transactions. Examples: ```ruby # HTTP calls within transaction User.transaction do user = User.new(user_params) user.save! # HTTP API call PaymentsService.charge!(user) end #=> raises Isolator::HTTPError # background job User.transaction do user.update!(confirmed_at: Time.now) UserMailer.successful_confirmation(user).deliver_later end #=> raises Isolator::BackgroundJobError ``` Of course, Isolator can detect _implicit_ transactions too. Consider this pretty common bad practice–enqueueing background job from `after_create` callback: ```ruby class Comment < ApplicationRecord # the good way is to use after_create_commit # (or not use callbacks at all) after_create :notify_author private def notify_author CommentMailer.comment_created(self).deliver_later end end Comment.create(text: "Mars is watching you!") #=> raises Isolator::BackgroundJobError ``` Isolator is supposed to be used in tests and on staging. ## Installation Add this line to your application's Gemfile: ```ruby # We suppose that Isolator is used in development and test # environments. group :development, :test do gem "isolator" end # Or you can add it to Gemfile with `require: false` # and require it manually in your code. # # This approach is useful when you want to use it in staging env too. gem "isolator", require: false ``` ## Usage Isolator is a plug-n-play tool, so, it begins to work right after required. However, there are some potential caveats: 1) Isolator tries to detect the environment automatically and includes only necessary adapters. Thus the order of loading gems matters: make sure that `isolator` is required in the end (NOTE: in Rails, all adapters loaded after application initialization). 2) Isolator does not distinguish framework-level adapters. For example, `:active_job` spy doesn't take into account which AJ adapter you use; if you are using a safe one (e.g. `Que`) just disable the `:active_job` adapter to avoid false negatives (i.e. `Isolator.adapters.active_job.disable!`). 3) Isolator tries to detect the `test` environment and slightly change its behavior: first, it respect _transactional tests_; secondly, error raising is turned on by default (see [below](#configuration)). ### Configuration ```ruby Isolator.configure do |config| # Specify a custom logger to log offenses config.logger = nil # Raise exception on offense config.raise_exceptions = false # true in test env # Send notifications to uniform_notifier config.send_notifications = false end ``` Isolator relys on [uniform_notifier][] to send custom notifications. **NOTE:** `uniform_notifier` should be installed separately (i.e., added to Gemfile). ### Supported ORMs - `ActiveRecord` >= 4.1 - `ROM::SQL` (only if Active Support instrumentation extenstion is loaded) ### Adapters Isolator has a bunch of built-in adapters: - `:http` – built on top of [Sniffer][] - `:active_job` - `:sidekiq` - `:resque` - `:resque_scheduler` - `:sucker_punch` - `:mailer` - `:webmock` – track mocked HTTP requests (unseen by Sniffer) in tests You can dynamically enable/disable adapters, e.g.: ```ruby # Disable HTTP adapter == do not spy on HTTP requests Isolator.adapters.http.disable! # Enable back Isolator.adapters.http.enable! ``` ### Ignore Offenses Since Isolator adapter is just a wrapper over original code, it may lead to false positives when there is another library patching the same behaviour. In that case you might want to ignore some offenses. Consider an example: we use Sidekiq along with [`sidekiq-postpone`](https://github.com/marshall-lee/sidekiq-postpone)–gem that patches `Sidekiq::Client#raw_push` and allows you to postpone jobs enqueueing (e.g. to enqueue everything when a transaction is commited–we don't want to raise exceptions in such situation). To ignore offenses when `sidekiq-postpone` is active, you can add an ignore `proc`: ```ruby Isolator.adapters.sidekiq.ignore_if { Thread.current[:sidekiq_postpone] } ``` You can add as many _ignores_ as you want, the offense is registered iff all of them return false. ## Custom Adapters An adapter is just a combination of a _method wrapper_ and lifecycle hooks. Suppose that you have a class `Danger` with a method `#explode`, which is not safe to be run within a DB transaction. Then you can _isolate_ it (i.e., register with Isolator): ```ruby # The first argument is a unique adapter id, # you can use it later to enable/disable the adapter # # The second argument is the method owner and # the third one is a method name. Isolotar.isolate :danger, Danger, :explode, **options # NOTE: if you want to isolate a class method, use signleton_class instead Isolator.isolate :danger, Danger.singleton_class, :explode, **options ``` Possible `options` are: - `exception_class` – an exception class to raise in case of offense - `exception_message` – custom exception message (could be specified without a class) You can also add some callbacks to be run before and after the transaction: ```ruby Isolator.before_isolate do # right after we enter the transaction end Isolator.after_isolate do # right after the transaction has been committed/rollbacked end ``` ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/isolator. ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). [Sniffer]: https://github.com/aderyabin/sniffer [uniform_notifier]: https://github.com/flyerhzm/uniform_notifier