Rails `after_commit` everywhere
Tags: rails, ruby, showdev
Published: 2018-02-21 16:01:05

---
title: Rails `after_commit` everywhere
published: true
canonical_url: https://evilmartians.com/chronicles/rails-after_commit-everywhere
description: How to run arbitrary code on successful DB transaction.
tags: rails, ruby, showdev
---

Recently I've released a new gem–[Isolator](https://github.com/palkan/isolator), which helps to detect [non-database side effects](https://www.reddit.com/r/ruby/comments/7yyhsv/isolator_nonatomic_interactions_within_db/dul7u6f/) during a database transaction.

Here is a quick example of such side effect:

```ruby
def pay(user, order_params)
  Order.transaction do
    order = order.new(order_params)
    order.save!

    # HTTP API call
    PaymentsService.charge!(user, order)
  end
end
```

What if our transaction fail right after we made an HTTP call (and charged a user)? Hardly anything good.

That's what [Isolator](https://github.com/palkan/isolator) is for: to prevent you from such situations.

Now, when we know what the problem is, how to fix it?

What about the following:

```ruby
def pay(user, order_params)
  Order.transaction do
    order = order.new(order_params)
    order.save!
  end

  # we don't reach this line when transaction fails
  PaymentsService.charge!(user, order)
end
```

Looks good, right? But what if you call `pay` somewhere in your code when the transaction has been already opened (i.e., in a _nested_ transaction):

```ruby
User.transaction do
  # do something with DB
  OrderService.pay(user, order_params)
  # whatever that may fail
end
```

Our HTTP call is made within a transaction. Again(

And that's just a simple example. I found much more sophisticated examples in the project I've been working on, and came out with the solution–**use ActiveRecord transaction callbacks**.

You've probably heard about [_transactional callbacks_](http://guides.rubyonrails.org/active_record_callbacks.html#transaction-callbacks) (such as `after_commit`). These callbacks are smart enough to run after the final (outer) `transaction`\* is committed.

\* Usually, there is one real transaction and nested transactions are implemented through _savepoints_ (see, for example, [PostgreSQL](https://postgrespro.com/docs/postgresql/9.6/sql-savepoint)).

How could these callbacks help us if they tight to ActiveRecord objects? Let's take a look at the source code.

ActiveRecord has a [`Transactions`](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/transactions.rb) module, which extends `Base` functionality.

It [wraps](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/transactions.rb#L302-L318) persistence methods into [`with_transaction_returning_status`](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/transactions.rb#L382), which in its turn call [`add_to_transaction`](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/transactions.rb#L363)–everything we need is there: we're adding our record (`self`, 'cause we're inside an ActiveRecord object) to the list of [`current_transaction.records`](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L56).


When the transaction (and not a savepoint) is committed, on every record from `records` we [invoke `committed!`](https://github.com/rails/rails/blob/v5.1.5/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L82) method. That's it!

So, in order to run arbitrary code after transaction commit, all we need is to add something _quacking_ like an AR record to the list of transaction records!

Let's add a special class called `AfterCommitWrap`:


```ruby
# Quack like an ActiveRecord and
# respond to `committed!`
class AfterCommitWrap
  def initialize
    @callback = Proc.new
  end

  def committed!(*)
    @callback.call
  end

  def before_committed!(*); end

  def rolledback!(*); end
end
```

Now it's time to use it:

```ruby
def pay(user, order_params)
  Order.transaction do
    order = order.new(order_params)
    order.save!

    ActiveRecord::Base.connection.add_transaction_record(
      AfterCommitWrap.new { PaymentsService.charge!(user, order) }
    )
  end
end
```

We are safe now. But the code looks too awkward, doesn't it? Let's add some ~~magic~~ sugar.

I'm a big fan of [refinements](https://ruby-doc.org/core-2.5.0/doc/syntax/refinements_rdoc.html) (yes, I am), and that's what I did to make this code look simpler and more beautiful:

```ruby
class AfterCommitWrap
  # ...
  module Helper
    refine ::Object do
      def after_commit(connection: ActiveRecord::Base.connection)
        connection.add_transaction_record(AfterCommitWrap.new(&Proc.new))
      end
    end
  end
end
```

And then:


```ruby
# activate our refinement
using AfterCommitWrap::Helper

def pay(user, order_params)
  Order.transaction do
    order = order.new(order_params)
    order.save!

    after_commit { PaymentsService.charge!(user, order) }
  end
end
```

That's it. Hope you like it)

-----

Read more dev articles on https://evilmartians.com/chronicles!