Skip to content

Commit aa9a683

Browse files
committed
fix: transaction state on rollback (#364)
Fixes #258
1 parent 11c2767 commit aa9a683

File tree

4 files changed

+75
-1
lines changed

4 files changed

+75
-1
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,7 @@ jobs:
8888
done
8989
cat ${{ github.workspace }}/setup.sql | cockroach sql --insecure
9090
- name: Test
91-
run: bundle exec rake test TESTOPTS='--profile=5'
91+
run: bundle exec rake test
92+
env:
93+
TESTOPTS: "--profile=5"
94+
RAILS_MINITEST_PLUGIN: "1" # Make sure that we use the minitest plugin for profiling.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Ongoing
44

5+
- Fix transaction state on rollback ([#364](https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/364))
6+
57
## 7.2.0 - 2024-09-24
68

79
- Add support for Rails 7.2 ([#337](https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/337))

lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ def within_new_transaction(isolation: nil, joinable: true, attempts: 0)
4545
within_new_transaction(isolation: isolation, joinable: joinable, attempts: attempts + 1) { yield }
4646
end
4747

48+
# OVERRIDE: the `rescue ActiveRecord::StatementInvalid` block is new, see comment.
49+
def rollback_transaction(transaction = nil)
50+
@connection.lock.synchronize do
51+
transaction ||= @stack.last
52+
begin
53+
transaction.rollback
54+
rescue ActiveRecord::StatementInvalid => err
55+
# This is important to make Active Record aware the record was not inserted/saved
56+
# Otherwise Active Record will assume save was successful and it doesn't retry the transaction
57+
# See this thread for more details:
58+
# https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/258#issuecomment-2256633329
59+
transaction.rollback_records if err.cause.is_a?(PG::NoActiveSqlTransaction)
60+
61+
raise
62+
ensure
63+
@stack.pop if @stack.last == transaction
64+
end
65+
transaction.rollback_records
66+
end
67+
end
68+
4869
def retryable?(error)
4970
return true if serialization_error?(error)
5071
return true if error.is_a? ActiveRecord::SerializationFailure

test/cases/transactions_test.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper_cockroachdb"
4+
5+
module CockroachDB
6+
class TransactionsTest < ActiveRecord::TestCase
7+
self.use_transactional_tests = false
8+
9+
class Avenger < ActiveRecord::Base
10+
singleton_class.attr_accessor :cyclic_barrier
11+
12+
validate :validate_unique_username
13+
14+
def validate_unique_username
15+
self.class.cyclic_barrier.wait
16+
duplicate = self.class.where(name: name).any?
17+
errors.add("Duplicate username!") if duplicate
18+
end
19+
end
20+
21+
def test_concurrent_insert_with_processes
22+
conn = ActiveRecord::Base.lease_connection
23+
conn.create_table :avengers, force: true do |t|
24+
t.string :name
25+
end
26+
ActiveRecord::Base.reset_column_information
27+
28+
avengers = %w[Hulk Thor Loki]
29+
Avenger.cyclic_barrier = Concurrent::CyclicBarrier.new(avengers.size - 1)
30+
Thread.current[:name] = "Main" # For debug logs.
31+
32+
assert_queries_match(/ROLLBACK/) do # Ensure we are properly testing the retry mechanism.
33+
avengers.map do |name|
34+
Thread.fork do
35+
Thread.current[:name] = name # For debug logs.
36+
Avenger.create!(name: name)
37+
end
38+
end.each(&:join)
39+
end
40+
41+
assert_equal avengers.size, Avenger.count
42+
ensure
43+
Thread.current[:name] = nil
44+
conn = ActiveRecord::Base.lease_connection
45+
conn.drop_table :avengers, if_exists: true
46+
end
47+
end
48+
end

0 commit comments

Comments
 (0)