From 0c4a25a8d49975f0c94ac071823cd5b1ab261752 Mon Sep 17 00:00:00 2001 From: Aryk Grosz Date: Tue, 13 Jun 2017 10:00:34 -0700 Subject: [PATCH] Add Sequel transactions and locking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add aasm features of ActiveRecord to Sequel! :) * All specs pass for sequel, activerecord, and mongoid. * Refactor Sequel models to be namespaced under “Sequel”. We should do something similar for ActiveRecord in the specs to avoid collisions. Addresses #474 (cherry picked from commit df97ce754f47864abbcb837227da5c4d0ff77289) --- lib/aasm/persistence/sequel_persistence.rb | 99 +++---- spec/models/sequel/complex_sequel_example.rb | 7 +- spec/models/sequel/invalid_persistor.rb | 52 ++++ spec/models/sequel/sequel_multiple.rb | 26 +- spec/models/sequel/sequel_simple.rb | 25 +- spec/models/sequel/silent_persistor.rb | 50 ++++ spec/models/sequel/transactor.rb | 112 +++++++ spec/models/sequel/validator.rb | 93 ++++++ spec/models/sequel/worker.rb | 12 + .../sequel_persistence_multiple_spec.rb | 4 +- .../persistence/sequel_persistence_spec.rb | 279 +++++++++++++++++- 11 files changed, 663 insertions(+), 96 deletions(-) create mode 100644 spec/models/sequel/invalid_persistor.rb create mode 100644 spec/models/sequel/silent_persistor.rb create mode 100644 spec/models/sequel/transactor.rb create mode 100644 spec/models/sequel/validator.rb create mode 100644 spec/models/sequel/worker.rb diff --git a/lib/aasm/persistence/sequel_persistence.rb b/lib/aasm/persistence/sequel_persistence.rb index a904bfe..c8f3b85 100644 --- a/lib/aasm/persistence/sequel_persistence.rb +++ b/lib/aasm/persistence/sequel_persistence.rb @@ -1,8 +1,10 @@ +require 'aasm/persistence/orm' module AASM module Persistence module SequelPersistence def self.included(base) base.send(:include, AASM::Persistence::Base) + base.send(:include, AASM::Persistence::ORM) base.send(:include, AASM::Persistence::SequelPersistence::InstanceMethods) end @@ -17,42 +19,42 @@ module AASM super end - # Returns the value of the aasm.attribute_name - called from aasm.current_state - # - # If it's a new record, and the aasm state column is blank it returns the initial state - # - # class Foo < Sequel::Model - # include AASM - # aasm :column => :status do - # state :opened - # state :closed - # end - # end - # - # foo = Foo.new - # foo.current_state # => :opened - # foo.close - # foo.current_state # => :closed - # - # foo = Foo[1] - # foo.current_state # => :opened - # foo.aasm_state = nil - # foo.current_state # => nil - # - # NOTE: intended to be called from an event - # - # This allows for nil aasm states - be sure to add validation to your model - def aasm_read_state(name=:default) - state = send(self.class.aasm(name).attribute_name) - if new? && state.to_s.strip.empty? - aasm(name).determine_state_name(self.class.aasm(name).initial_state) - elsif state.nil? - nil - else - state.to_sym + def aasm_raise_invalid_record + raise Sequel::ValidationFailed.new(self) + end + + def aasm_new_record? + new? + end + + # Returns nil if fails silently + # http://sequel.jeremyevans.net/rdoc/classes/Sequel/Model/InstanceMethods.html#method-i-save + def aasm_save + !save(raise_on_failure: false).nil? + end + + def aasm_read_attribute(name) + send(name) + end + + def aasm_write_attribute(name, value) + send("#{name}=", value) + end + + def aasm_transaction(requires_new, requires_lock) + self.class.db.transaction(savepoint: requires_new) do + if requires_lock + # http://sequel.jeremyevans.net/rdoc/classes/Sequel/Model/InstanceMethods.html#method-i-lock-21 + requires_lock.is_a?(String) ? lock!(requires_lock) : lock! + end + yield end end + def aasm_update_column(attribute_name, value) + this.update(attribute_name => value) + end + # Ensures that if the aasm_state column is nil and the record is new # that the initial state gets populated before validation on create # @@ -72,39 +74,10 @@ module AASM AASM::StateMachineStore.fetch(self.class, true).machine_names.each do |state_machine_name| aasm(state_machine_name).enter_initial_state if (new? || values.key?(self.class.aasm(state_machine_name).attribute_name)) && - send(self.class.aasm(state_machine_name).attribute_name).to_s.strip.empty? + send(self.class.aasm(state_machine_name).attribute_name).to_s.strip.empty? end end - # Writes state to the state column and persists it to the database - # - # foo = Foo[1] - # foo.aasm.current_state # => :opened - # foo.close! - # foo.aasm.current_state # => :closed - # Foo[1].aasm.current_state # => :closed - # - # NOTE: intended to be called from an event - def aasm_write_state state, name=:default - aasm_column = self.class.aasm(name).attribute_name - update_fields({aasm_column => state.to_s}, [aasm_column], missing: :skip) - end - - # Writes state to the state column, but does not persist it to the database - # - # foo = Foo[1] - # foo.aasm.current_state # => :opened - # foo.close - # foo.aasm.current_state # => :closed - # Foo[1].aasm.current_state # => :opened - # foo.save - # foo.aasm.current_state # => :closed - # Foo[1].aasm.current_state # => :closed - # - # NOTE: intended to be called from an event - def aasm_write_state_without_persistence state, name=:default - send("#{self.class.aasm(name).attribute_name}=", state.to_s) - end end end end diff --git a/spec/models/sequel/complex_sequel_example.rb b/spec/models/sequel/complex_sequel_example.rb index 9cca371..8859588 100644 --- a/spec/models/sequel/complex_sequel_example.rb +++ b/spec/models/sequel/complex_sequel_example.rb @@ -1,4 +1,4 @@ -db = Sequel.connect(SEQUEL_DB) +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) # if you want to see the statements while running the spec enable the following line # db.loggers << Logger.new($stderr) @@ -8,8 +8,8 @@ db.create_table(:complex_sequel_examples) do String :right end -class ComplexSequelExample < Sequel::Model(db) - set_dataset(:complex_sequel_examples) +module Sequel +class ComplexExample < Sequel::Model(:complex_sequel_examples) include AASM @@ -43,3 +43,4 @@ class ComplexSequelExample < Sequel::Model(db) end end +end diff --git a/spec/models/sequel/invalid_persistor.rb b/spec/models/sequel/invalid_persistor.rb new file mode 100644 index 0000000..b209618 --- /dev/null +++ b/spec/models/sequel/invalid_persistor.rb @@ -0,0 +1,52 @@ +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) + +[:invalid_persistors, :multiple_invalid_persistors].each do |table_name| + db.create_table(table_name) do + primary_key :id + String "name" + String "status" + end +end + +module Sequel + class InvalidPersistor < Sequel::Model(:invalid_persistors) + plugin :validation_helpers + + include AASM + aasm :column => :status, :skip_validation_on_save => true do + state :sleeping, :initial => true + state :running + event :run do + transitions :to => :running, :from => :sleeping + end + event :sleep do + transitions :to => :sleeping, :from => :running + end + end + + def validate + super + validates_presence :name + end + end + + class MultipleInvalidPersistor < Sequel::Model(:multiple_invalid_persistors) + plugin :validation_helpers + + include AASM + aasm :left, :column => :status, :skip_validation_on_save => true do + state :sleeping, :initial => true + state :running + event :run do + transitions :to => :running, :from => :sleeping + end + event :sleep do + transitions :to => :sleeping, :from => :running + end + end + def validate + super + validates_presence :name + end + end +end diff --git a/spec/models/sequel/sequel_multiple.rb b/spec/models/sequel/sequel_multiple.rb index c17209e..88511e3 100644 --- a/spec/models/sequel/sequel_multiple.rb +++ b/spec/models/sequel/sequel_multiple.rb @@ -1,4 +1,4 @@ -db = Sequel.connect(SEQUEL_DB) +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) # if you want to see the statements while running the spec enable the following line # db.loggers << Logger.new($stderr) @@ -6,20 +6,20 @@ db.create_table(:multiples) do primary_key :id String :status end +module Sequel + class Multiple < Sequel::Model(:multiples) + include AASM -class SequelMultiple < Sequel::Model(db) - set_dataset(:multiples) - include AASM + attr_accessor :default - attr_accessor :default - - aasm :left, :column => :status - aasm :left do - state :alpha, :initial => true - state :beta - state :gamma - event :release do - transitions :from => [:alpha, :beta, :gamma], :to => :beta + aasm :left, :column => :status + aasm :left do + state :alpha, :initial => true + state :beta + state :gamma + event :release do + transitions :from => [:alpha, :beta, :gamma], :to => :beta + end end end end diff --git a/spec/models/sequel/sequel_simple.rb b/spec/models/sequel/sequel_simple.rb index bc84f2b..bd5b844 100644 --- a/spec/models/sequel/sequel_simple.rb +++ b/spec/models/sequel/sequel_simple.rb @@ -1,4 +1,4 @@ -db = Sequel.connect(SEQUEL_DB) +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) # if you want to see the statements while running the spec enable the following line # db.loggers << Logger.new($stderr) @@ -7,19 +7,20 @@ db.create_table(:simples) do String :status end -class SequelSimple < Sequel::Model(db) - set_dataset(:simples) - include AASM +module Sequel + class Simple < Sequel::Model(:simples) + include AASM - attr_accessor :default + attr_accessor :default - aasm :column => :status - aasm do - state :alpha, :initial => true - state :beta - state :gamma - event :release do - transitions :from => [:alpha, :beta, :gamma], :to => :beta + aasm :column => :status + aasm do + state :alpha, :initial => true + state :beta + state :gamma + event :release do + transitions :from => [:alpha, :beta, :gamma], :to => :beta + end end end end diff --git a/spec/models/sequel/silent_persistor.rb b/spec/models/sequel/silent_persistor.rb new file mode 100644 index 0000000..2e8b22a --- /dev/null +++ b/spec/models/sequel/silent_persistor.rb @@ -0,0 +1,50 @@ +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) + +[:silent_persistors, :multiple_silent_persistors].each do |t| + db.create_table(t) do + primary_key :id + String "name" + String "status" + end +end + +module Sequel + class SilentPersistor < Sequel::Model(:silent_persistors) + plugin :validation_helpers + + include AASM + aasm :column => :status, :whiny_persistence => false do + state :sleeping, :initial => true + state :running + event :run do + transitions :to => :running, :from => :sleeping + end + event :sleep do + transitions :to => :sleeping, :from => :running + end + end + def validate + validates_presence :name + end + end + + class MultipleSilentPersistor< Sequel::Model(:multiple_silent_persistors) + plugin :validation_helpers + + include AASM + aasm :left, :column => :status, :whiny_persistence => false do + state :sleeping, :initial => true + state :running + event :run do + transitions :to => :running, :from => :sleeping + end + event :sleep do + transitions :to => :sleeping, :from => :running + end + end + + def validate + validates_presence :name + end + end +end diff --git a/spec/models/sequel/transactor.rb b/spec/models/sequel/transactor.rb new file mode 100644 index 0000000..a0ee0ff --- /dev/null +++ b/spec/models/sequel/transactor.rb @@ -0,0 +1,112 @@ +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) + +[:transactors, :no_lock_transactors, :lock_transactors, :lock_no_wait_transactors, :multiple_transactors].each do |table_name| + db.create_table(table_name) do + primary_key :id + String "name" + String "status" + Fixnum "worker_id" + end +end + +module Sequel + class Transactor < Sequel::Model(:transactors) + + many_to_one :worker, class: 'Sequel::Worker' + + include AASM + aasm :column => :status do + state :sleeping, :initial => true + state :running, :before_enter => :start_worker, :after_enter => :fail + + event :run do + transitions :to => :running, :from => :sleeping + end + end + + private + + def start_worker + worker.update(status: 'running') + end + + def fail + raise StandardError.new('failed on purpose') + end + + end + + class NoLockTransactor < Sequel::Model(:no_lock_transactors) + + many_to_one :worker, class: 'Sequel::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 < Sequel::Model(:lock_transactors) + + many_to_one :worker, class: 'Sequel::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 < Sequel::Model(:lock_no_wait_transactors) + + many_to_one :worker, class: 'Sequel::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 < Sequel::Model(:multiple_transactors) + + many_to_one :worker, class: 'Sequel::Worker' + + include AASM + aasm :left, :column => :status do + state :sleeping, :initial => true + state :running, :before_enter => :start_worker, :after_enter => :fail + + event :run do + transitions :to => :running, :from => :sleeping + end + end + + private + + def start_worker + worker.update(:status, 'running') + end + + def fail + raise StandardError.new('failed on purpose') + end + + end +end diff --git a/spec/models/sequel/validator.rb b/spec/models/sequel/validator.rb new file mode 100644 index 0000000..4528495 --- /dev/null +++ b/spec/models/sequel/validator.rb @@ -0,0 +1,93 @@ +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) + +db.create_table(:validators) do + primary_key :id + String "name" + String "status" + Fixnum "worker_id" +end + +module Sequel + class Validator < Sequel::Model(:validators) + plugin :validation_helpers + + attr_accessor :after_all_transactions_performed, + :after_transaction_performed_on_fail, + :after_transaction_performed_on_run, + :before_all_transactions_performed, + :before_transaction_performed_on_fail, + :before_transaction_performed_on_run, + :invalid + + def validate + super + errors.add(:validator, "invalid") if invalid + validates_presence :name + end + + include AASM + + aasm :column => :status, :whiny_persistence => true do + before_all_transactions :before_all_transactions + after_all_transactions :after_all_transactions + + state :sleeping, :initial => true + state :running + state :failed, :after_enter => :fail + + event :run, :after_commit => :change_name! do + after_transaction do + @after_transaction_performed_on_run = true + end + + before_transaction do + @before_transaction_performed_on_run = true + end + + transitions :to => :running, :from => :sleeping + end + + event :sleep do + after_commit do |name| + change_name_on_sleep name + end + transitions :to => :sleeping, :from => :running + end + + event :fail do + after_transaction do + @after_transaction_performed_on_fail = true + end + + before_transaction do + @before_transaction_performed_on_fail = true + end + + transitions :to => :failed, :from => [:sleeping, :running] + end + end + + def change_name! + self.name = "name changed" + save(raise_on_failure: true) + end + + def change_name_on_sleep name + self.name = name + save(raise_on_failure: true) + end + + def fail + raise StandardError.new('failed on purpose') + end + + def after_all_transactions + @after_all_transactions_performed = true + end + + def before_all_transactions + @before_all_transactions_performed = true + end + end + +end diff --git a/spec/models/sequel/worker.rb b/spec/models/sequel/worker.rb new file mode 100644 index 0000000..eecddf6 --- /dev/null +++ b/spec/models/sequel/worker.rb @@ -0,0 +1,12 @@ +db = Sequel::DATABASES.first || Sequel.connect(SEQUEL_DB) + +db.create_table(:workers) do + primary_key :id + String "name" + String "status" +end + +module Sequel + class Worker < Sequel::Model(:workers) + end +end diff --git a/spec/unit/persistence/sequel_persistence_multiple_spec.rb b/spec/unit/persistence/sequel_persistence_multiple_spec.rb index 4119014..1183a18 100644 --- a/spec/unit/persistence/sequel_persistence_multiple_spec.rb +++ b/spec/unit/persistence/sequel_persistence_multiple_spec.rb @@ -8,7 +8,7 @@ if defined?(Sequel) end before(:all) do - @model = SequelMultiple + @model = Sequel::Multiple end describe "instance methods" do @@ -93,7 +93,7 @@ if defined?(Sequel) describe "complex example" do it "works" do - record = ComplexSequelExample.new + record = Sequel::ComplexExample.new expect(record.aasm(:left).current_state).to eql :one expect(record.left).to be_nil expect(record.aasm(:right).current_state).to eql :alpha diff --git a/spec/unit/persistence/sequel_persistence_spec.rb b/spec/unit/persistence/sequel_persistence_spec.rb index 4b2857b..2b42b96 100644 --- a/spec/unit/persistence/sequel_persistence_spec.rb +++ b/spec/unit/persistence/sequel_persistence_spec.rb @@ -1,14 +1,13 @@ require 'spec_helper' - if defined?(Sequel) describe 'sequel' do - + Dir[File.dirname(__FILE__) + "/../../models/sequel/*.rb"].sort.each do |f| require File.expand_path(f) end before(:all) do - @model = SequelSimple + @model = Sequel::Simple end describe "instance methods" do @@ -91,5 +90,279 @@ if defined?(Sequel) end end + describe 'transitions with persistence' do + + it "should work for valid models" do + valid_object = Sequel::Validator.create(:name => 'name') + expect(valid_object).to be_sleeping + valid_object.status = :running + expect(valid_object).to be_running + end + + it 'should not store states for invalid models' do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect { validator.run! }.to raise_error(Sequel::ValidationFailed) + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should not store states for invalid models silently if configured' do + validator = Sequel::SilentPersistor.create(:name => 'name') + expect(validator).to be_valid + expect(validator).to be_sleeping + + validator.name = nil + expect(validator).not_to be_valid + expect(validator.run!).to be_falsey + expect(validator).to be_sleeping + + validator.reload + expect(validator).not_to be_running + expect(validator).to be_sleeping + + validator.name = 'another name' + expect(validator).to be_valid + expect(validator.run!).to be_truthy + expect(validator).to be_running + + validator.reload + expect(validator).to be_running + expect(validator).not_to be_sleeping + end + + it 'should store states for invalid models if configured' do + persistor = Sequel::InvalidPersistor.create(:name => 'name') + expect(persistor).to be_valid + expect(persistor).to be_sleeping + + persistor.name = nil + expect(persistor).not_to be_valid + expect(persistor.run!).to be_truthy + expect(persistor).to be_running + + persistor = Sequel::InvalidPersistor[persistor.id] + persistor.valid? + expect(persistor).to be_valid + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + + persistor.reload + expect(persistor).to be_running + expect(persistor).not_to be_sleeping + end + + describe 'pessimistic locking' do + let(:worker) { Sequel::Worker.create(:name => 'worker', :status => 'sleeping') } + + subject { transactor.run! } + + context 'no lock' do + let(:transactor) { Sequel::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) { Sequel::LockTransactor.create(:name => 'lock_transactor', :worker => worker) } + + it 'should invoke lock!' do + expect(transactor).to receive(:lock!).and_call_original + subject + end + end + + context 'a FOR UPDATE NOWAIT lock' do + let(:transactor) { Sequel::LockNoWaitTransactor.create(:name => 'lock_no_wait_transactor', :worker => worker) } + + it 'should invoke lock! with FOR UPDATE NOWAIT' do + # TODO: With and_call_original, get an error with syntax, should look into it. + expect(transactor).to receive(:lock!).with('FOR UPDATE NOWAIT')# .and_call_original + subject + end + end + end + + describe 'transactions' do + let(:worker) { Sequel::Worker.create(:name => 'worker', :status => 'sleeping') } + let(:transactor) { Sequel::Transactor.create(:name => 'transactor', :worker => worker) } + + it 'should rollback all changes' do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + expect {transactor.run!}.to raise_error(StandardError, 'failed on purpose') + expect(transactor).to be_running + expect(worker.reload.status).to eq('sleeping') + end + + context "nested transactions" do + it "should rollback all changes in nested transaction" do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + Sequel::Worker.db.transaction do + expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose') + end + + expect(transactor).to be_running + expect(worker.reload.status).to eq('sleeping') + end + + it "should only rollback changes in the main transaction not the nested one" do + # change configuration to not require new transaction + AASM::StateMachineStore[Sequel::Transactor][:default].config.requires_new_transaction = false + + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + Sequel::Worker.db.transaction do + expect { transactor.run! }.to raise_error(StandardError, 'failed on purpose') + end + expect(transactor).to be_running + expect(worker.reload.status).to eq('running') + end + end + + describe "after_commit callback" do + it "should fire :after_commit if transaction was successful" do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_sleeping + + validator.run! + expect(validator).to be_running + expect(validator.name).to eq("name changed") + + validator.sleep!("sleeper") + expect(validator).to be_sleeping + expect(validator.name).to eq("sleeper") + end + + it "should not fire :after_commit if transaction failed" do + validator = Sequel::Validator.create(:name => 'name') + expect { validator.fail! }.to raise_error(StandardError, 'failed on purpose') + expect(validator.name).to eq("name") + end + + it "should not fire :after_commit if validation failed when saving object" do + validator = Sequel::Validator.create(:name => 'name') + validator.invalid = true + expect { validator.run! }.to raise_error(Sequel::ValidationFailed, 'validator invalid') + expect(validator).to be_sleeping + expect(validator.name).to eq("name") + end + + it "should not fire if not saving" do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_sleeping + validator.run + expect(validator).to be_running + expect(validator.name).to eq("name") + end + end + + describe 'before and after transaction callbacks' do + [:after, :before].each do |event_type| + describe "#{event_type}_transaction callback" do + it "should fire :#{event_type}_transaction if transaction was successful" do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_sleeping + + expect { validator.run! }.to change { validator.send("#{event_type}_transaction_performed_on_run") }.from(nil).to(true) + expect(validator).to be_running + end + + it "should fire :#{event_type}_transaction if transaction failed" do + validator = Sequel::Validator.create(:name => 'name') + expect do + begin + validator.fail! + rescue => ignored + end + end.to change { validator.send("#{event_type}_transaction_performed_on_fail") }.from(nil).to(true) + expect(validator).to_not be_running + end + + it "should not fire :#{event_type}_transaction if not saving" do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_sleeping + expect { validator.run }.to_not change { validator.send("#{event_type}_transaction_performed_on_run") } + expect(validator).to be_running + expect(validator.name).to eq("name") + end + end + end + end + + describe 'before and after all transactions callbacks' do + [:after, :before].each do |event_type| + describe "#{event_type}_all_transactions callback" do + it "should fire :#{event_type}_all_transactions if transaction was successful" do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_sleeping + + expect { validator.run! }.to change { validator.send("#{event_type}_all_transactions_performed") }.from(nil).to(true) + expect(validator).to be_running + end + + it "should fire :#{event_type}_all_transactions if transaction failed" do + validator = Sequel::Validator.create(:name => 'name') + expect do + begin + validator.fail! + rescue => ignored + end + end.to change { validator.send("#{event_type}_all_transactions_performed") }.from(nil).to(true) + expect(validator).to_not be_running + end + + it "should not fire :#{event_type}_all_transactions if not saving" do + validator = Sequel::Validator.create(:name => 'name') + expect(validator).to be_sleeping + expect { validator.run }.to_not change { validator.send("#{event_type}_all_transactions_performed") } + expect(validator).to be_running + expect(validator.name).to eq("name") + end + end + end + end + + context "when not persisting" do + it 'should not rollback all changes' do + expect(transactor).to be_sleeping + expect(worker.status).to eq('sleeping') + + # Notice here we're calling "run" and not "run!" with a bang. + expect {transactor.run}.to raise_error(StandardError, 'failed on purpose') + expect(transactor).to be_running + expect(worker.reload.status).to eq('running') + end + + it 'should not create a database transaction' do + expect(transactor.class).not_to receive(:transaction) + expect {transactor.run}.to raise_error(StandardError, 'failed on purpose') + end + end + end + end + end end