Skip to content

Commit

Permalink
Allow to call transactional callbacks directly on module
Browse files Browse the repository at this point in the history
  • Loading branch information
andrebarretofv authored and Envek committed Aug 5, 2021
1 parent 43991e9 commit 2c48296
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 54 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Allow to call transactional callbacks directly on `AfterCommitEverywhere` module:

```ruby
AfterCommitEverywhere.after_commit { puts "If you see me then transaction has been successfully commited!" }
```

- Allow to call `in_transaction?` helper method from instance methods in classes that includes `AfterCommitEverywhere` module.

## 1.0.0 (2021-02-17)

Declare gem as stable. No changes since 0.1.5.
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
after_commit_everywhere (1.0.0)
activerecord (>= 4.2)
activesupport

GEM
remote: https://rubygems.org/
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ ActiveRecord::Base.transaction do
end
```

Or call it directly on module:

```ruby
AfterCommitEverywhere.after_commit { puts "We're all done!" }
```

That's it!

But the main benefit is that it works with nested `transaction` blocks (may be even spread across many files in your codebase):
Expand Down Expand Up @@ -107,6 +113,12 @@ If called outside transaction will raise an exception!

Please keep in mind ActiveRecord's [limitations for rolling back nested transactions](http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions).

### Available helper methods

#### `in_transaction?`

Returns `true` when called inside open transaction, `false` otherwise.

### FAQ

#### Does it works with transactional_test or DatabaseCleaner
Expand Down
1 change: 1 addition & 0 deletions after_commit_everywhere.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_dependency "activerecord", ">= 4.2"
spec.add_dependency "activesupport"
spec.add_development_dependency "appraisal"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "isolator", "~> 0.7"
Expand Down
113 changes: 59 additions & 54 deletions lib/after_commit_everywhere.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "active_record"
require "active_support/core_ext/module/delegation"

require "after_commit_everywhere/version"
require "after_commit_everywhere/wrap"
Expand All @@ -12,65 +13,69 @@
module AfterCommitEverywhere
class NotInTransaction < RuntimeError; end

# Runs +callback+ after successful commit of outermost transaction for
# database +connection+.
#
# If called outside transaction it will execute callback immediately.
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
# @param callback [#call] Callback to be executed
# @return void
def after_commit(connection: ActiveRecord::Base.connection, &callback)
AfterCommitEverywhere.register_callback(
connection: connection,
name: __method__,
callback: callback,
no_tx_action: :execute,
)
end
delegate :after_commit, :before_commit, :after_rollback, to: AfterCommitEverywhere
delegate :in_transaction?, to: AfterCommitEverywhere

# Runs +callback+ before committing of outermost transaction for +connection+.
#
# If called outside transaction it will execute callback immediately.
#
# Available only since Ruby on Rails 5.0. See https://github.com/rails/rails/pull/18936
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
# @param callback [#call] Callback to be executed
# @return void
def before_commit(connection: ActiveRecord::Base.connection, &callback)
if ActiveRecord::VERSION::MAJOR < 5
raise NotImplementedError, "#{__method__} works only with Rails 5.0+"
class << self
# Runs +callback+ after successful commit of outermost transaction for
# database +connection+.
#
# If called outside transaction it will execute callback immediately.
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
# @param callback [#call] Callback to be executed
# @return void
def after_commit(connection: ActiveRecord::Base.connection, &callback)
register_callback(
connection: connection,
name: __method__,
callback: callback,
no_tx_action: :execute,
)
end

AfterCommitEverywhere.register_callback(
connection: connection,
name: __method__,
callback: callback,
no_tx_action: :warn_and_execute,
)
end
# Runs +callback+ before committing of outermost transaction for +connection+.
#
# If called outside transaction it will execute callback immediately.
#
# Available only since Ruby on Rails 5.0. See https://github.com/rails/rails/pull/18936
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
# @param callback [#call] Callback to be executed
# @return void
def before_commit(connection: ActiveRecord::Base.connection, &callback)
if ActiveRecord::VERSION::MAJOR < 5
raise NotImplementedError, "#{__method__} works only with Rails 5.0+"
end

# Runs +callback+ after rolling back of transaction or savepoint (if declared
# in nested transaction) for database +connection+.
#
# Caveat: do not raise +ActivRecord::Rollback+ in nested transaction block!
# See http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
# @param callback [#call] Callback to be executed
# @return void
# @raise [NotInTransaction] if called outside transaction.
def after_rollback(connection: ActiveRecord::Base.connection, &callback)
AfterCommitEverywhere.register_callback(
connection: connection,
name: __method__,
callback: callback,
no_tx_action: :exception,
)
end
register_callback(
connection: connection,
name: __method__,
callback: callback,
no_tx_action: :warn_and_execute,
)
end

class << self
# Runs +callback+ after rolling back of transaction or savepoint (if declared
# in nested transaction) for database +connection+.
#
# Caveat: do not raise +ActivRecord::Rollback+ in nested transaction block!
# See http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
# @param callback [#call] Callback to be executed
# @return void
# @raise [NotInTransaction] if called outside transaction.
def after_rollback(connection: ActiveRecord::Base.connection, &callback)
register_callback(
connection: connection,
name: __method__,
callback: callback,
no_tx_action: :exception,
)
end

# @api private
def register_callback(connection:, name:, no_tx_action:, callback:)
raise ArgumentError, "Provide callback to #{name}" unless callback

Expand Down
48 changes: 48 additions & 0 deletions spec/after_commit_everywhere_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,54 @@
end
end

describe "#in_transaction?" do
subject { example_class.new.in_transaction? }

it "returns true when in transaction" do
ActiveRecord::Base.transaction do
is_expected.to be_truthy
end
end

it "returns false when not in transaction" do
is_expected.to be_falsey
end
end

describe ".after_commit" do
subject do
described_class.after_commit do
handler.call
expect(ActiveRecord::Base.connection.transaction_open?).to be_falsey
end
end

it "executes code only after commit" do
ActiveRecord::Base.transaction do
subject
expect(handler).not_to have_received(:call)
end
expect(handler).to have_received(:call)
end

# Here we're checking only happy path. for other tests see "#after_commit"
end

describe ".after_rollback" do
subject { described_class.after_rollback { handler.call } }

it "executes code only after rollback" do
ActiveRecord::Base.transaction do
subject
expect(handler).not_to have_received(:call)
raise ActiveRecord::Rollback
end
expect(handler).to have_received(:call)
end

# Here we're checking only happy path. for other tests see "#after_rollback"
end

describe ".in_transaction?" do
subject { described_class.in_transaction? }

Expand Down

0 comments on commit 2c48296

Please sign in to comment.