Skip to content

Commit

Permalink
Events still not persisted if nested transaction used
Browse files Browse the repository at this point in the history
A developer can start a transaction outside of RES. We need to handle
this situation properly!

http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions

transaction calls can be nested. By default, this makes all database statements in the nested transaction block become part of the parent transaction. For example, the following behavior may be surprising:

User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

creates both “Kotori” and “Nemu”. Reason is the ActiveRecord::Rollback
exception in the nested block does not issue a ROLLBACK. Since these
exceptions are captured in transaction blocks, the parent block does not see
it and the real transaction is committed.

AAAAAAAAAAAAAaaaaaaaa !!!!

In order to get a ROLLBACK for the nested transaction you may ask for a real
sub-transaction by passing requires_new: true. If anything goes wrong, the
database rolls back to the beginning of the sub-transaction without rolling
back the parent transaction. If we add it to the previous example:

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

only “Kotori” is created. This works on MySQL and PostgreSQL.
SQLite3 version >= '3.6.8' also supports it.
  • Loading branch information
paneq committed Oct 30, 2017
1 parent 37b6b08 commit fc2ba56
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def append_to_stream(events, stream_name, expected_version)
raise RubyEventStore::InvalidExpectedVersion
end

ActiveRecord::Base.transaction do
ActiveRecord::Base.transaction(requires_new: true) do
in_stream = events.flat_map.with_index do |event, index|
position = unless expected_version.equal?(:any)
expected_version + index + POSITION_SHIFT
Expand Down
21 changes: 21 additions & 0 deletions rails_event_store_active_record/spec/event_repository_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,27 @@ module RailsEventStoreActiveRecord
end
end

specify "nested transaction - events still not persisted if append failed" do
repository = EventRepository.new
repository.append_to_stream([
event = TestDomainEvent.new(event_id: SecureRandom.uuid),
], 'stream', :none)

ActiveRecord::Base.transaction do
expect do
repository.append_to_stream([
TestDomainEvent.new(
event_id: '9bedf448-e4d0-41a3-a8cd-f94aec7aa763'
),
], 'stream', :none)
end.to raise_error(RubyEventStore::WrongExpectedEventVersion)
expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
expect(repository.read_all_streams_forward(:head, 2)).to eq([event])
end
expect(repository.has_event?('9bedf448-e4d0-41a3-a8cd-f94aec7aa763')).to be_falsey
expect(repository.read_all_streams_forward(:head, 2)).to eq([event])
end

def cleanup_concurrency_test
ActiveRecord::Base.connection_pool.disconnect!
end
Expand Down

0 comments on commit fc2ba56

Please sign in to comment.