Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix after commit within nested transaction vol2 #668

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ appraise 'rails_5.2' do
end

appraise 'norails' do
gem 'sqlite3', '~> 1.3', '>= 1.3.5', platforms: :ruby
gem 'rails', install_if: false
gem 'after_commit_everywhere', install_if: false
gem 'sequel'
gem 'redis-objects'
end
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ gemspec

gem 'sqlite3', '~> 1.3.5', :platforms => :ruby
gem 'rails', '5.1.4'
gem 'after_commit_everywhere', '~> 0.1', '>= 0.1.5'
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ job.aasm.current_state # stage3
### Multiple state machines per class

Multiple state machines per class are supported. Be aware though that _AASM_ has been
built with one state machine per class in mind. Nonetheless, here's how to do it (see below). Please note that you will need to specify database columns for where your pertinent states will be stored - we have specified two columns `move_state` and `work_state` in the example below. See the [Column name & migration](https://github.com/aasm/aasm#column-name--migration) section for further info.
built with one state machine per class in mind. Nonetheless, here's how to do it (see below). Please note that you will need to specify database columns for where your pertinent states will be stored - we have specified two columns `move_state` and `work_state` in the example below. See the [Column name & migration](https://github.com/aasm/aasm#column-name--migration) section for further info.

```ruby
class SimpleMultipleExample
Expand Down Expand Up @@ -981,6 +981,12 @@ job.run
job.save! #notify_about_running_job is not run
```

Please note that `:after_commit` AASM callbacks behaves around custom implementation
of transaction pattern rather than a real-life DB transaction. This fact still causes
the race conditions and redundant callback calls within nested transaction. In order
to fix that it's highly recommended to add `gem 'after_commit_everywhere', '~> 0.1', '>= 0.1.5'`
to your `Gemfile`.

If you want to encapsulate state changes within an own transaction, the behavior
of this nested transaction might be confusing. Take a look at
[ActiveRecord Nested Transactions](http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html)
Expand Down Expand Up @@ -1092,7 +1098,7 @@ end

### Log State Changes

Logging state change can be done using [paper_trail](https://github.com/paper-trail-gem/paper_trail) gem
Logging state change can be done using [paper_trail](https://github.com/paper-trail-gem/paper_trail) gem

Example of implementation can be found here [https://github.com/nitsujri/aasm-papertrail-example](https://github.com/nitsujri/aasm-papertrail-example)

Expand Down
1 change: 1 addition & 0 deletions gemfiles/norails.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3", ">= 1.3.5", platforms: :ruby
gem "rails", install_if: false
gem "after_commit_everywhere", install_if: false
gem "sequel"
gem "redis-objects"

Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_4.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "4.2.5"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "nokogiri", "1.6.8.1", platforms: [:ruby_19]
gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
gem "mongoid", "~> 4.0"
Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_4.2_mongoid_5.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "4.2.5"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "mime-types", "~> 2", platforms: [:ruby_19, :jruby]
gem "mongoid", "~> 5.0"
gem "activerecord-jdbcsqlite3-adapter", "1.3.24", platforms: :jruby
Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_4.2_nobrainer.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "4.2.5"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "nobrainer", "~> 0.33.0"

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_5.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "5.0.0"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "mongoid", "~> 6.0"
gem "sequel"
gem "dynamoid", "~> 1.3", platforms: :ruby
Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_5.0_nobrainer.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "5.0.0"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "nobrainer", "~> 0.33.0"

gemspec path: "../"
1 change: 1 addition & 0 deletions gemfiles/rails_5.1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "5.1"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "mongoid", "~>6.0"
gem "sequel"
gem "dynamoid", "~> 1.3", platforms: :ruby
Expand Down
1 change: 1 addition & 0 deletions gemfiles/rails_5.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source "https://rubygems.org"

gem "sqlite3", "~> 1.3.5", platforms: :ruby
gem "rails", "5.2"
gem "after_commit_everywhere", "~> 0.1", ">= 0.1.5"
gem "mongoid", "~>6.0"
gem "sequel"
gem "dynamoid", "~>2.2", platforms: :ruby
Expand Down
13 changes: 13 additions & 0 deletions lib/aasm/persistence/active_record_persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ module ActiveRecordPersistence
# end
#
def self.included(base)
begin
require 'after_commit_everywhere'
raise LoadError unless Gem::Version.new(::AfterCommitEverywhere::VERSION) >= Gem::Version.new('0.1.5')

base.send(:include, ::AfterCommitEverywhere) unless base.include?(::AfterCommitEverywhere)
base.send(:alias_method, :aasm_execute_after_commit, :after_commit)
rescue LoadError
warn <<-MSG
[DEPRECATION] :after_commit AASM callback is not safe in terms of race conditions and redundant calls.
Please add `gem 'after_commit_everywhere', '~> 0.1', '>= 0.1.5'` to your Gemfile in order to fix that.
MSG
end

base.send(:include, AASM::Persistence::Base)
base.send(:include, AASM::Persistence::ORM)
base.send(:include, AASM::Persistence::ActiveRecordPersistence::InstanceMethods)
Expand Down
42 changes: 23 additions & 19 deletions lib/aasm/persistence/orm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def aasm_supports_transactions?
true
end

def aasm_execute_after_commit
yield
end

def aasm_write_state_attribute(state, name=:default)
aasm_write_attribute(self.class.aasm(name).attribute_name, aasm_raw_attribute_value(state, name))
end
Expand Down Expand Up @@ -116,32 +120,32 @@ def requires_lock?(state_machine_name)

# Returns true if event was fired successfully and transaction completed.
def aasm_fire_event(state_machine_name, name, options, *args, &block)
if aasm_supports_transactions? && options[:persist]
event = self.class.aasm(state_machine_name).state_machine.events[name]
event.fire_callbacks(:before_transaction, self, *args)
event.fire_global_callbacks(:before_all_transactions, self, *args)

begin
success = if options[:persist] && use_transactions?(state_machine_name)
aasm_transaction(requires_new?(state_machine_name), requires_lock?(state_machine_name)) do
super
end
else
return super unless aasm_supports_transactions? && options[:persist]

event = self.class.aasm(state_machine_name).state_machine.events[name]
event.fire_callbacks(:before_transaction, self, *args)
event.fire_global_callbacks(:before_all_transactions, self, *args)

begin
success = if options[:persist] && use_transactions?(state_machine_name)
aasm_transaction(requires_new?(state_machine_name), requires_lock?(state_machine_name)) do
super
end
else
super
end

if success
if success
aasm_execute_after_commit do
event.fire_callbacks(:after_commit, self, *args)
event.fire_global_callbacks(:after_all_commits, self, *args)
end

success
ensure
event.fire_callbacks(:after_transaction, self, *args)
event.fire_global_callbacks(:after_all_transactions, self, *args)
end
else
super

success
ensure
event.fire_callbacks(:after_transaction, self, *args)
event.fire_global_callbacks(:after_all_transactions, self, *args)
end
end

Expand Down
32 changes: 32 additions & 0 deletions spec/unit/persistence/active_record_persistence_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,38 @@
expect(validator).to be_running
expect(validator.name).to eq("name")
end

context "nested transaction" do
it "should fire :after_commit if root transaction was successful" do
validator = Validator.create(:name => 'name')
expect(validator).to be_sleeping

validator.transaction do
validator.run!
expect(validator.name).to eq("name")
expect(validator).to be_running
end

expect(validator.name).to eq("name changed")
expect(validator.reload).to be_running
end

it "should not fire :after_commit if root transaction failed" do
validator = Validator.create(:name => 'name')
expect(validator).to be_sleeping

validator.transaction do
validator.run!
expect(validator.name).to eq("name")
expect(validator).to be_running

raise ActiveRecord::Rollback, "failed on purpose"
end

expect(validator.name).to eq("name")
expect(validator.reload).to be_sleeping
end
end
end

describe 'before and after transaction callbacks' do
Expand Down