Skip to content

Commit a1bd698

Browse files
p-mongop
authored andcommitted
Fix RUBY-1661 Non-mongo exception in abort_transaction does not change session state (#1207)
1 parent b630188 commit a1bd698

File tree

5 files changed

+103
-0
lines changed

5 files changed

+103
-0
lines changed

docs/tutorials/ruby-driver-transactions.txt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,39 @@ To commit or abort a transaction, call ``commit_transaction`` or
110110

111111
session.abort_transaction
112112

113+
Note: an outstanding transaction can hold locks to various objects in the
114+
server, such as the database. For example, the drop call in the following
115+
snippet will hang for `transactionLifetimeLimitSeconds
116+
<https://docs.mongodb.com/manual/reference/parameters/#param.transactionLifetimeLimitSeconds>`_
117+
seconds (default 60) until the server expires and aborts the transaction:
118+
119+
.. code-block:: ruby
120+
121+
c1 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db)
122+
session = c1.start_session
123+
c1['foo'].insert_one(test: 1)
124+
session.start_transaction
125+
c1['foo'].insert_one({test: 2}, session: session)
126+
127+
c2 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db)
128+
# hangs
129+
c2.database.drop
130+
131+
Since transactions are associated with server-side sessions, closing the client
132+
does not abort a transaction that this client initiated - the application must
133+
either call ``abort_transaction`` or wait for the transaction to time out on
134+
the server side. In addition to committing or aborting the transaction, an
135+
application can also end the session which will abort a transaction on this
136+
session if one is in progress:
137+
138+
.. code-block:: ruby
139+
140+
session.end_session
141+
142+
c2 = Mongo::Client.new(['127.0.0.1:27017']).use(:test_db)
143+
# ok
144+
c2.database.drop
145+
113146

114147
Retrying Commits
115148
----------------

lib/mongo/session.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,9 @@ def abort_transaction
697697
raise
698698
rescue Mongo::Error
699699
@state = TRANSACTION_ABORTED_STATE
700+
rescue Exception
701+
@state = TRANSACTION_ABORTED_STATE
702+
raise
700703
end
701704
end
702705

spec/mongo/session_transaction_spec.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,54 @@ class SessionTransactionSpecError < StandardError; end
2222
collection.delete_many
2323
end
2424

25+
describe '#abort_transaction' do
26+
require_topology :replica_set
27+
28+
context 'when a non-Mongo error is raised' do
29+
before do
30+
collection.insert_one({foo: 1})
31+
end
32+
33+
it 'propagates the exception and sets state to transaction aborted' do
34+
session.start_transaction
35+
collection.insert_one({foo: 1}, session: session)
36+
expect(session).to receive(:write_with_retry).and_raise(SessionTransactionSpecError)
37+
expect do
38+
session.abort_transaction
39+
end.to raise_error(SessionTransactionSpecError)
40+
expect(session.send(:within_states?, Mongo::Session::TRANSACTION_ABORTED_STATE)).to be true
41+
42+
# Since we failed abort_transaction call, the transaction is still
43+
# outstanding. It will cause subsequent tests to stall until it times
44+
# out on the server side. End the session to force the server
45+
# to close the transaction.
46+
kill_all_server_sessions
47+
end
48+
end
49+
50+
context 'when a Mongo error is raised' do
51+
before do
52+
collection.insert_one({foo: 1})
53+
end
54+
55+
it 'swallows the exception and sets state to transaction aborted' do
56+
session.start_transaction
57+
collection.insert_one({foo: 1}, session: session)
58+
expect(session).to receive(:write_with_retry).and_raise(Mongo::Error::SocketError)
59+
expect do
60+
session.abort_transaction
61+
end.not_to raise_error
62+
expect(session.send(:within_states?, Mongo::Session::TRANSACTION_ABORTED_STATE)).to be true
63+
64+
# Since we failed abort_transaction call, the transaction is still
65+
# outstanding. It will cause subsequent tests to stall until it times
66+
# out on the server side. End the session to force the server
67+
# to close the transaction.
68+
kill_all_server_sessions
69+
end
70+
end
71+
end
72+
2573
describe '#with_transaction' do
2674
context 'callback successful' do
2775
it 'commits' do

spec/spec_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
RSpec.configure do |config|
1616
config.include(Authorization)
1717
config.extend(Constraints)
18+
19+
config.before(:all) do
20+
if ClusterConfig.instance.fcv_ish >= '3.6'
21+
kill_all_server_sessions
22+
end
23+
end
1824
end
1925

2026
# Determine whether the test clients are connecting to a sharded cluster

spec/support/common_shortcuts.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ def declare_topology_double
1212
end
1313

1414
module InstanceMethods
15+
def kill_all_server_sessions
16+
begin
17+
ClientRegistry.instance.global_client('root_authorized').command(killAllSessions: [])
18+
# killAllSessions also kills the implicit session which the driver uses
19+
# to send this command, as a result it always fails
20+
rescue Mongo::Error::OperationFailure => e
21+
# "operation was interrupted"
22+
unless e.code == 11601
23+
raise
24+
end
25+
end
26+
end
27+
1528
def wait_for_all_servers(cluster)
1629
# Cluster waits for initial round of sdam until the primary
1730
# is discovered, which means by the time a connection is obtained

0 commit comments

Comments
 (0)