Skip to content

Commit

Permalink
pessmistic lock work
Browse files Browse the repository at this point in the history
model lock and reload

fix mis-spell

trivial commit to ping travis ci

documentation

spelling

better table

more edits with midu

correct spec descriptions
  • Loading branch information
HoyaBoya committed Feb 5, 2016
1 parent b9999f2 commit 4e91f49
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 18 deletions.
35 changes: 35 additions & 0 deletions README.md
Expand Up @@ -693,6 +693,41 @@ end

which then leads to `transaction(:requires_new => false)`, the Rails default.

### Pessimistic Locking

AASM supports [Active Record pessimistic locking via `with_lock`](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html#method-i-with_lock) for database persistence layers.

| Option | Purpose |
| ------ | ------- |
| `false` (default) | No lock is obtained | |
| `true` | Obtain a blocking pessimistic lock e.g. `FOR UPDATE` |
| String | Obtain a lock based on the SQL string e.g. `FOR UPDATE NOWAIT` |


```ruby
class Job < ActiveRecord::Base
include AASM

aasm :requires_lock => true do
...
end

...
end
```

```ruby
class Job < ActiveRecord::Base
include AASM

aasm :requires_lock => 'FOR UPDATE NOWAIT' do
...
end

...
end
```


### Column name & migration

Expand Down
5 changes: 5 additions & 0 deletions lib/aasm/base.rb
Expand Up @@ -24,6 +24,11 @@ def initialize(klass, name, state_machine, options={}, &block)
# use requires_new for nested transactions (in ActiveRecord)
configure :requires_new_transaction, true

# use pessimistic locking (in ActiveRecord)
# true for FOR UPDATE lock
# string for a specific lock type i.e. FOR UPDATE NOWAIT
configure :requires_lock, false

# set to true to forbid direct assignment of aasm_state column (in ActiveRecord)
configure :no_direct_assignment, false

Expand Down
5 changes: 4 additions & 1 deletion lib/aasm/configuration.rb
Expand Up @@ -15,9 +15,12 @@ class Configuration
# for ActiveRecord: use requires_new for nested transactions?
attr_accessor :requires_new_transaction

# for ActiveRecord: use pessimistic locking
attr_accessor :requires_lock

# forbid direct assignment in aasm_state column (in ActiveRecord)
attr_accessor :no_direct_assignment

attr_accessor :enum
end
end
end
14 changes: 13 additions & 1 deletion lib/aasm/persistence/active_record_persistence.rb
Expand Up @@ -165,7 +165,15 @@ def aasm_fire_event(state_machine_name, name, options, *args, &block)
end

begin
success = options[:persist] ? self.class.transaction(:requires_new => requires_new?(state_machine_name)) { super } : super
success = if options[:persist]
self.class.transaction(:requires_new => requires_new?(state_machine_name)) do
lock!(requires_lock?(state_machine_name)) if requires_lock?(state_machine_name)
super
end
else
super
end

if options[:persist] && success
event.fire_callbacks(:after_commit, self, *args)
event.fire_global_callbacks(:after_all_commits, self, *args)
Expand All @@ -184,6 +192,10 @@ def requires_new?(state_machine_name)
AASM::StateMachine[self.class][state_machine_name].config.requires_new_transaction
end

def requires_lock?(state_machine_name)
AASM::StateMachine[self.class][state_machine_name].config.requires_lock
end

def aasm_validate_states
AASM::StateMachine[self.class].keys.each do |state_machine_name|
unless aasm_skipping_validations(state_machine_name)
Expand Down
27 changes: 11 additions & 16 deletions spec/database.rb
Expand Up @@ -17,24 +17,19 @@
t.string "right"
end

ActiveRecord::Migration.create_table "validators", :force => true do |t|
t.string "name"
t.string "status"
end
ActiveRecord::Migration.create_table "multiple_validators", :force => true do |t|
t.string "name"
t.string "status"
%w(validators multiple_validators).each do |table_name|
ActiveRecord::Migration.create_table table_name, :force => true do |t|
t.string "name"
t.string "status"
end
end

ActiveRecord::Migration.create_table "transactors", :force => true do |t|
t.string "name"
t.string "status"
t.integer "worker_id"
end
ActiveRecord::Migration.create_table "multiple_transactors", :force => true do |t|
t.string "name"
t.string "status"
t.integer "worker_id"
%w(transactors no_lock_transactors lock_transactors lock_no_wait_transactors multiple_transactors).each do |table_name|
ActiveRecord::Migration.create_table table_name, :force => true do |t|
t.string "name"
t.string "status"
t.integer "worker_id"
end
end

ActiveRecord::Migration.create_table "workers", :force => true do |t|
Expand Down
48 changes: 48 additions & 0 deletions spec/models/transactor.rb
Expand Up @@ -26,6 +26,54 @@ def fail

end

class NoLockTransactor < ActiveRecord::Base

belongs_to :worker

include AASM

aasm :column => :status do
state :sleeping, :initial => true
state :running

event :run do
transitions :to => :running, :from => :sleeping
end
end
end

class LockTransactor < ActiveRecord::Base

belongs_to :worker

include AASM

aasm :column => :status, requires_lock: true do
state :sleeping, :initial => true
state :running

event :run do
transitions :to => :running, :from => :sleeping
end
end
end

class LockNoWaitTransactor < ActiveRecord::Base

belongs_to :worker

include AASM

aasm :column => :status, requires_lock: 'FOR UPDATE NOWAIT' do
state :sleeping, :initial => true
state :running

event :run do
transitions :to => :running, :from => :sleeping
end
end
end

class MultipleTransactor < ActiveRecord::Base

belongs_to :worker
Expand Down
33 changes: 33 additions & 0 deletions spec/unit/persistence/active_record_persistence_spec.rb
Expand Up @@ -425,6 +425,39 @@
expect(persistor).not_to be_sleeping
end

describe 'pessimistic locking' do
let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') }

subject { transactor.run! }

context 'no lock' do
let(:transactor) { NoLockTransactor.create!(:name => 'no_lock_transactor', :worker => worker) }

it 'should not invoke lock!' do
expect(transactor).to_not receive(:lock!)
subject
end
end

context 'a default lock' do
let(:transactor) { LockTransactor.create!(:name => 'lock_transactor', :worker => worker) }

it 'should invoke lock! with true' do
expect(transactor).to receive(:lock!).with(true).and_call_original
subject
end
end

context 'a FOR UPDATE NOWAIT lock' do
let(:transactor) { LockNoWaitTransactor.create!(:name => 'lock_no_wait_transactor', :worker => worker) }

it 'should invoke lock! with FOR UPDATE NOWAIT' do
expect(transactor).to receive(:lock!).with('FOR UPDATE NOWAIT').and_call_original
subject
end
end
end

describe 'transactions' do
let(:worker) { Worker.create!(:name => 'worker', :status => 'sleeping') }
let(:transactor) { Transactor.create!(:name => 'transactor', :worker => worker) }
Expand Down

0 comments on commit 4e91f49

Please sign in to comment.