Skip to content

Commit 69de00b

Browse files
MONGOID-5530 Add transaction method (#5508)
1 parent 475d5eb commit 69de00b

15 files changed

+780
-304
lines changed

lib/config/locales/en.yml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@ en:
357357
in the session block share the same driver client. For example, a model may have a different
358358
client specified in its 'store_in' options.\n\n"
359359
invalid_session_nesting:
360-
message: "A session was started while another session was being used."
361-
summary: "Sessions cannot be nested. Only one session can be used in a thread at once."
362-
resolution: "Only use one session at a time; sessions cannot be nested."
360+
message: "A session was started while another session was being used on this client."
361+
summary: "Sessions cannot be nested. Only one session can be used in a thread per client."
362+
resolution: "Only use one session per client at a time; sessions cannot be nested."
363363
invalid_storage_options:
364364
message: "Invalid options passed to %{klass}.store_in: %{options}."
365365
summary: "The :store_in macro takes only a hash of parameters with
@@ -381,6 +381,10 @@ en:
381381
for Date, DateTime, and Time objects. When this is a String it needs
382382
to be valid for Time.parse. Other objects must be valid to pass to
383383
Time.local."
384+
invalid_transaction_nesting:
385+
message: "A transaction was started while another transaction was being used on this client."
386+
summary: "Transactions cannot be nested. Only one transaction can be used in a thread per client."
387+
resolution: "Only use one transaction per client at a time; transactions cannot be nested."
384388
inverse_not_found:
385389
message: "When adding a(n) %{klass} to %{base}#%{name}, Mongoid could
386390
not determine the inverse foreign key to set. The attempted key was
@@ -617,6 +621,19 @@ en:
617621
accepts_nested_attributes_for :%{association}, limit: %{limit}.
618622
Consider raising this limit or making sure no more are sent than
619623
the set value."
624+
transactions_not_supported:
625+
message: "Transactions are not supported by the connected server(s)."
626+
summary: "A session was attempted to be used with a MongoDB server version
627+
that doesn't support transactions. Transactions are supported in MongoDB
628+
server versions 3.6 and higher."
629+
resolution: "Verify that all servers in your deployment are at least
630+
version 3.6 or don't attempt to use transactions with older server versions."
631+
transaction_error:
632+
message: "Transaction failed with the following error: %{error}."
633+
summary: "The transaction failed because MongoDB server or MongoDB driver
634+
raised an unexpected error."
635+
resolution: "Consult with Ruby driver documentation
636+
and MongoDB documentation."
620637
unknown_attribute:
621638
message: "Attempted to set a value for '%{name}' which is not
622639
allowed on the model %{klass}."

lib/mongoid/clients/options.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,21 @@ def mongo_client
4040
end
4141

4242
def persistence_context
43-
PersistenceContext.get(self) ||
44-
PersistenceContext.get(self.class) ||
45-
PersistenceContext.new(self.class)
43+
if embedded? && !_root?
44+
_root.persistence_context
45+
else
46+
PersistenceContext.get(self) ||
47+
PersistenceContext.get(self.class) ||
48+
PersistenceContext.new(self.class)
49+
end
4650
end
4751

4852
def persistence_context?
49-
!!(PersistenceContext.get(self) || PersistenceContext.get(self.class))
53+
if embedded? && !_root?
54+
_root.persistence_context?
55+
else
56+
!!(PersistenceContext.get(self) || PersistenceContext.get(self.class))
57+
end
5058
end
5159

5260
private

lib/mongoid/clients/sessions.rb

Lines changed: 61 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,11 @@
33
module Mongoid
44
module Clients
55

6-
# Encapsulates behavior for getting a session from the client of a model class or instance,
7-
# setting the session on the current thread, and yielding to a block.
8-
# The session will be closed after the block completes or raises an error.
6+
# Encapsulates behavior for using sessions and transactions.
97
module Sessions
108

11-
# Execute a block within the context of a session.
12-
#
13-
# @example Execute some operations in the context of a session.
14-
# band.with_session(causal_consistency: true) do
15-
# band.records << Record.create
16-
# band.name = 'FKA Twigs'
17-
# band.save
18-
# band.reload
19-
# end
20-
#
21-
# @param [ Hash ] options The session options. Please see the driver
22-
# documentation for the available session options.
23-
#
24-
# @note You cannot do any operations in the block using models or objects
25-
# that use a different client; the block will execute all operations
26-
# in the context of the implicit session and operations on any models using
27-
# another client will fail. For example, if you set a client using store_in on a
28-
# particular model and execute an operation on it in the session context block,
29-
# that operation can't use the block's session and an error will be raised.
30-
# An error will also be raised if sessions are nested.
31-
#
32-
# @raise [ Errors::InvalidSessionUse ] If an operation is attempted on a model using another
33-
# client from which the session was started or if sessions are nested.
34-
#
35-
# @return [ Object ] The result of calling the block.
36-
#
37-
# @yieldparam [ Mongo::Session ] The session being used for the block.
38-
def with_session(options = {})
39-
if Threaded.get_session
40-
raise Mongoid::Errors::InvalidSessionUse.new(:invalid_session_nesting)
41-
end
42-
session = persistence_context.client.start_session(options)
43-
Threaded.set_session(session)
44-
yield(session)
45-
rescue Mongo::Error::InvalidSession => ex
46-
if Mongo::Error::SessionsNotSupported === ex
47-
raise Mongoid::Errors::InvalidSessionUse.new(:sessions_not_supported)
48-
end
49-
raise Mongoid::Errors::InvalidSessionUse.new(:invalid_session_use)
50-
ensure
51-
Threaded.clear_session
52-
end
53-
54-
private
55-
56-
def _session
57-
Threaded.get_session
9+
def self.included(base)
10+
base.include(ClassMethods)
5811
end
5912

6013
module ClassMethods
@@ -72,40 +25,83 @@ module ClassMethods
7225
# @param [ Hash ] options The session options. Please see the driver
7326
# documentation for the available session options.
7427
#
75-
# @note You cannot do any operations in the block using models or objects
76-
# that use a different client; the block will execute all operations
77-
# in the context of the implicit session and operations on any models using
78-
# another client will fail. For example, if you set a client using store_in on a
79-
# particular model and execute an operation on it in the session context block,
80-
# that operation can't use the block's session and an error will be raised.
81-
# You also cannot nest sessions.
82-
#
8328
# @raise [ Errors::InvalidSessionUse ] If an operation is attempted on a model using another
8429
# client from which the session was started or if sessions are nested.
8530
#
8631
# @return [ Object ] The result of calling the block.
8732
#
8833
# @yieldparam [ Mongo::Session ] The session being used for the block.
8934
def with_session(options = {})
90-
if Threaded.get_session
91-
raise Mongoid::Errors::InvalidSessionUse.new(:invalid_session_nesting)
35+
if Threaded.get_session(client: persistence_context.client)
36+
raise Mongoid::Errors::InvalidSessionNesting.new
9237
end
9338
session = persistence_context.client.start_session(options)
94-
Threaded.set_session(session)
39+
Threaded.set_session(session, client: persistence_context.client)
9540
yield(session)
9641
rescue Mongo::Error::InvalidSession => ex
9742
if Mongo::Error::SessionsNotSupported === ex
98-
raise Mongoid::Errors::InvalidSessionUse.new(:sessions_not_supported)
43+
raise Mongoid::Errors::SessionsNotSupported.new
44+
else
45+
raise ex
46+
end
47+
rescue Mongo::Error::OperationFailure => ex
48+
if (ex.code == 40415 && ex.server_message =~ /startTransaction/) ||
49+
(ex.code == 20 && ex.server_message =~ /Transaction/)
50+
then
51+
raise Mongoid::Errors::TransactionsNotSupported.new
52+
else
53+
raise ex
9954
end
100-
raise Mongoid::Errors::InvalidSessionUse.new(:invalid_session_use)
10155
ensure
102-
Threaded.clear_session
56+
Threaded.clear_session(client: persistence_context.client)
57+
end
58+
59+
# Executes a block within the context of a transaction.
60+
#
61+
# If the block does not raise an error, the transaction is committed.
62+
# If an error is raised, the transaction is aborted. The error is passed on
63+
# except for the `Mongoid::Errors::Rollback`. This error is not passed on,
64+
# so you can raise is if you want to deliberately rollback the transaction.
65+
#
66+
# @param [ Hash ] options The transaction options. Please see the driver
67+
# documentation for the available session options.
68+
# @param [ Hash ] session_options The session options. A MongoDB
69+
# transaction must be started inside a session, therefore a session will
70+
# be started. Please see the driver documentation for the available session options.
71+
#
72+
# @raise [ Mongoid::Errors::InvalidTransactionNesting ] If the transaction is
73+
# opened on a client that already has an open transaction.
74+
# @raise [ Mongoid::Errors::TransactionsNotSupported ] If MongoDB deployment
75+
# the client is connected to does not support transactions.
76+
# @raise [ Mongoid::Errors::TransactionError ] If there is an error raised
77+
# by MongoDB deployment or MongoDB driver.
78+
#
79+
# @yield Provided block will be executed inside a transaction.
80+
def transaction(options = {}, session_options: {})
81+
with_session(session_options) do |session|
82+
begin
83+
session.start_transaction(options)
84+
yield
85+
session.commit_transaction
86+
rescue Mongoid::Errors::Rollback
87+
session.abort_transaction
88+
rescue Mongoid::Errors::InvalidSessionNesting
89+
# Session should be ended here.
90+
raise Mongoid::Errors::InvalidTransactionNesting.new
91+
rescue Mongo::Error::InvalidSession, Mongo::Error::InvalidTransactionOperation => e
92+
session.abort_transaction
93+
raise Mongoid::Errors::TransactionError(e)
94+
rescue StandardError => e
95+
session.abort_transaction
96+
raise e
97+
end
98+
end
10399
end
104100

105101
private
106102

107103
def _session
108-
Threaded.get_session
104+
Threaded.get_session(client: persistence_context.client)
109105
end
110106
end
111107
end

lib/mongoid/errors.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@
3737
require "mongoid/errors/invalid_relation"
3838
require "mongoid/errors/invalid_relation_option"
3939
require "mongoid/errors/invalid_scope"
40-
require "mongoid/errors/invalid_session_use"
40+
require "mongoid/errors/invalid_session_nesting"
4141
require "mongoid/errors/invalid_set_polymorphic_relation"
4242
require "mongoid/errors/invalid_storage_options"
4343
require "mongoid/errors/invalid_time"
44+
require "mongoid/errors/invalid_transaction_nesting"
4445
require "mongoid/errors/inverse_not_found"
4546
require "mongoid/errors/mixed_relations"
4647
require "mongoid/errors/mixed_client_configuration"
@@ -56,8 +57,12 @@
5657
require "mongoid/errors/no_client_hosts"
5758
require "mongoid/errors/readonly_attribute"
5859
require "mongoid/errors/readonly_document"
60+
require "mongoid/errors/rollback"
61+
require "mongoid/errors/sessions_not_supported"
5962
require "mongoid/errors/scope_overwrite"
6063
require "mongoid/errors/too_many_nested_attribute_records"
64+
require "mongoid/errors/transaction_error"
65+
require "mongoid/errors/transactions_not_supported"
6166
require "mongoid/errors/unknown_attribute"
6267
require "mongoid/errors/unknown_model"
6368
require "mongoid/errors/unsaved_document"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
# This error is raised when a session is attempted to be used with a model whose client already
7+
# has an opened session.
8+
class InvalidSessionNesting < MongoidError
9+
10+
# Create the error.
11+
def initialize
12+
super(compose_message('invalid_session_nesting'))
13+
end
14+
end
15+
end
16+
end

lib/mongoid/errors/invalid_session_use.rb

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
# This error is raised when a transaction is attempted to be used with a model whose client already
7+
# has an opened transaction.
8+
class InvalidTransactionNesting < MongoidError
9+
10+
# Create the error.
11+
def initialize
12+
super(compose_message('invalid_transaction_nesting'))
13+
end
14+
end
15+
end
16+
end

lib/mongoid/errors/rollback.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
# This error should be raised to deliberately rollback a transaction without
7+
# passing on an exception.
8+
# Normally, raising an exception inside a Mongoid transaction causes rolling
9+
# the MongoDB transaction back, and the exception is passed on.
10+
# If Mongoid::Error::Rollback exception is raised, then the MongoDB
11+
# transaction will be rolled back, without passing on the exception.
12+
class Rollback < MongoidError; end
13+
end
14+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
# This error is raised when a session is attempted to be used with a model whose client cannot use it since
7+
# the mongodb deployment doesn't support sessions.
8+
class SessionsNotSupported < MongoidError
9+
10+
# Create the error.
11+
def initialize
12+
super('sessions_not_supported')
13+
end
14+
end
15+
end
16+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Errors
5+
6+
class TransactionError < MongoidError
7+
8+
# This error is raised when a transaction failed because of an unexpected
9+
# error.
10+
#
11+
# @param [ StandardError ] error Error that caused the transaction failure.
12+
def initialize(error)
13+
super(
14+
compose_message(
15+
'transaction_error',
16+
{ error: "#{error.class}: #{error.message}" }
17+
)
18+
)
19+
end
20+
end
21+
end
22+
end

0 commit comments

Comments
 (0)