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 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
|
# 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(:subscriber) do
Mrss::EventSubscriber.new(name: 'SessionTransactionSpec')
end
let(:client) do
authorized_client.tap do |client|
client.subscribe(Mongo::Monitoring::COMMAND, subscriber)
end
end
let(:session) do
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
context 'csot' do
context 'when csot is enabled' do
context 'when timeout_ms is set to zero' do
it 'sets with_transaction_deadline to infinite' do
session.with_transaction(timeout_ms: 0) do
expect(session.with_transaction_deadline).to be_zero
end
end
it 'does not sent maxTimeMS' do
session.with_transaction(timeout_ms: 0) do
collection.insert_one({ a: 1 }, session: session)
end
event = subscriber.single_command_started_event('insert', database_name: collection.database.name)
expect(event.command['maxTimeMS']).to be_nil
end
end
context 'when timeout_ms is set to a positive value' do
before do
allow(Mongo::Utils).to receive(:monotonic_time).and_return(0)
end
it 'sets with_transaction_deadline to the specified value' do
session.with_transaction(timeout_ms: 1000) do
expect(session.with_transaction_deadline).to be_within(0.1).of(1000 / 1000.0)
end
end
it 'sends maxTimeMS with the operation' do
session.with_transaction(timeout_ms: 1_000) do
collection.insert_one({ a: 1 }, session: session)
end
event = subscriber.single_command_started_event('insert', database_name: collection.database.name)
expect(event.command['maxTimeMS']).not_to be_nil
expect(event.command['maxTimeMS']).to be <= 1_000
end
end
end
context 'when csot is disabled' do
it 'does not set with_transaction_deadline' do
session.with_transaction do
expect(session.with_transaction_deadline).to be_nil
end
end
end
end
end
end
|