1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
|
# frozen_string_literal: true
# rubocop:todo all
require 'spec_helper'
class SessionTransactionSpecError < StandardError; end
describe Mongo::Session do
require_wired_tiger
min_server_fcv '4.0'
require_topology :replica_set, :sharded
let(:session) do
authorized_client.start_session(session_options)
end
let(:session_options) do
{}
end
let(:collection) do
authorized_client['session-transaction-test']
end
before do
collection.delete_many
end
describe 'start_transaction' do
context 'when topology is sharded and server is < 4.2' do
max_server_fcv '4.1'
require_topology :sharded
it 'raises an error' do
expect { session.start_transaction }.to raise_error(Mongo::Error::TransactionsNotSupported, /sharded transactions require server version/)
end
end
end
describe '#abort_transaction' do
require_topology :replica_set
context 'when a non-Mongo error is raised' do
before do
collection.insert_one({foo: 1})
end
it 'propagates the exception and sets state to transaction aborted' do
session.start_transaction
collection.insert_one({foo: 1}, session: session)
expect(session).to receive(:write_with_retry).and_raise(SessionTransactionSpecError)
expect do
session.abort_transaction
end.to raise_error(SessionTransactionSpecError)
expect(session.send(:within_states?, Mongo::Session::TRANSACTION_ABORTED_STATE)).to be true
# Since we failed abort_transaction call, the transaction is still
# outstanding. It will cause subsequent tests to stall until it times
# out on the server side. End the session to force the server
# to close the transaction.
kill_all_server_sessions
end
end
context 'when a Mongo error is raised' do
before do
collection.insert_one({foo: 1})
end
it 'swallows the exception and sets state to transaction aborted' do
session.start_transaction
collection.insert_one({foo: 1}, session: session)
expect(session).to receive(:write_with_retry).and_raise(Mongo::Error::SocketError)
expect do
session.abort_transaction
end.not_to raise_error
expect(session.send(:within_states?, Mongo::Session::TRANSACTION_ABORTED_STATE)).to be true
# Since we failed abort_transaction call, the transaction is still
# outstanding. It will cause subsequent tests to stall until it times
# out on the server side. End the session to force the server
# to close the transaction.
kill_all_server_sessions
end
end
end
describe '#with_transaction' do
require_topology :replica_set
context 'callback successful' do
it 'commits' do
session.with_transaction do
collection.insert_one(a: 1)
end
result = collection.find(a: 1).first
expect(result[:a]).to eq(1)
end
it 'propagates callback\'s return value' do
rv = session.with_transaction do
42
end
expect(rv).to eq(42)
end
end
context 'callback raises' do
it 'propagates the exception' do
expect do
session.with_transaction do
raise SessionTransactionSpecError, 'test error'
end
end.to raise_error(SessionTransactionSpecError, 'test error')
end
end
context 'callback aborts transaction' do
it 'does not raise exceptions and propagates callback\'s return value' do
rv = session.with_transaction do
session.abort_transaction
42
end
expect(rv).to eq(42)
end
end
context 'timeout with callback raising TransientTransactionError' do
max_example_run_time 7
it 'times out' do
start = Mongo::Utils.monotonic_time
expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start)
expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 1)
expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 2)
expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 200)
allow(session).to receive('check_transactions_supported!').and_return true
expect do
session.with_transaction do
exc = Mongo::Error::OperationFailure.new('timeout test')
exc.add_label('TransientTransactionError')
raise exc
end
end.to raise_error(Mongo::Error::OperationFailure, 'timeout test')
end
end
%w(UnknownTransactionCommitResult TransientTransactionError).each do |label|
context "timeout with commit raising with #{label}" do
max_example_run_time 7
# JRuby seems to burn through the monotonic time expectations
# very quickly and the retries of the transaction get the original
# time which causes the transaction to be stuck there.
fails_on_jruby
before do
# create collection if it does not exist
collection.insert_one(a: 1)
end
retry_test
it 'times out' do
start = Mongo::Utils.monotonic_time
11.times do |i|
expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + i)
end
expect(Mongo::Utils).to receive(:monotonic_time).ordered.and_return(start + 200)
allow(session).to receive('check_transactions_supported!').and_return true
exc = Mongo::Error::OperationFailure.new('timeout test')
exc.add_label(label)
expect(session).to receive(:commit_transaction).and_raise(exc).at_least(:once)
expect do
session.with_transaction do
collection.insert_one(a: 2)
end
end.to raise_error(Mongo::Error::OperationFailure, 'timeout test')
end
end
end
context 'callback breaks out of with_tx loop' do
it 'aborts transaction' do
expect(session).to receive(:start_transaction).and_call_original
expect(session).to receive(:abort_transaction).and_call_original
expect(session).to receive(:log_warn).and_call_original
session.with_transaction do
break
end
end
end
context 'application timeout around with_tx' do
it 'keeps session in a working state' do
session
collection.insert_one(a: 1)
expect do
Timeout.timeout(1, SessionTransactionSpecError) do
session.with_transaction do
sleep 2
end
end
end.to raise_error(SessionTransactionSpecError)
session.with_transaction do
collection.insert_one(timeout_around_with_tx: 2)
end
expect(collection.find(timeout_around_with_tx: 2).first).not_to be nil
end
end
end
end
|