Skip to content

Commit 33cee24

Browse files
MONGOID-5531 Add transaction callbacks (#5519)
1 parent c94c76b commit 33cee24

File tree

10 files changed

+558
-33
lines changed

10 files changed

+558
-33
lines changed

lib/mongoid/clients/sessions.rb

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ module Clients
55

66
# Encapsulates behavior for using sessions and transactions.
77
module Sessions
8-
98
def self.included(base)
109
base.include(ClassMethods)
1110
end
@@ -46,8 +45,7 @@ def with_session(options = {})
4645
end
4746
rescue Mongo::Error::OperationFailure => ex
4847
if (ex.code == 40415 && ex.server_message =~ /startTransaction/) ||
49-
(ex.code == 20 && ex.server_message =~ /Transaction/)
50-
then
48+
(ex.code == 20 && ex.server_message =~ /Transaction/)
5149
raise Mongoid::Errors::TransactionsNotSupported.new
5250
else
5351
raise ex
@@ -82,27 +80,66 @@ def transaction(options = {}, session_options: {})
8280
begin
8381
session.start_transaction(options)
8482
yield
85-
session.commit_transaction
83+
commit_transaction(session)
8684
rescue Mongoid::Errors::Rollback
87-
session.abort_transaction
85+
abort_transaction(session)
8886
rescue Mongoid::Errors::InvalidSessionNesting
8987
# Session should be ended here.
9088
raise Mongoid::Errors::InvalidTransactionNesting.new
9189
rescue Mongo::Error::InvalidSession, Mongo::Error::InvalidTransactionOperation => e
92-
session.abort_transaction
90+
abort_transaction(session)
9391
raise Mongoid::Errors::TransactionError(e)
9492
rescue StandardError => e
95-
session.abort_transaction
93+
abort_transaction(session)
9694
raise e
9795
end
9896
end
9997
end
10098

10199
private
102100

101+
# @return [ Mongo::Session ] Session for the current client.
103102
def _session
104103
Threaded.get_session(client: persistence_context.client)
105104
end
105+
106+
# This method should be used to detect whether a persistence operation
107+
# is executed inside transaction or not.
108+
#
109+
# Currently this method is used to detect when +after_commit+ callbacks
110+
# should be triggered. If we introduce implicit transactions and
111+
# therefore do not need to handle two different ways of triggering callbacks,
112+
# we may want to remove this method.
113+
#
114+
# @return [ true | false ] Whether there is a session for the current
115+
# client, and there is a transaction in progress for this session.
116+
def in_transaction?
117+
_session&.in_transaction? || false
118+
end
119+
120+
# Commits the active transaction on the session, and calls
121+
# after_commit callbacks on modified documents.
122+
#
123+
# @param [ Mongo::Session ] session Session on which
124+
# a transaction is started.
125+
def commit_transaction(session)
126+
session.commit_transaction
127+
Threaded.clear_modified_documents(session).each do |doc|
128+
doc.run_after_callbacks(:commit)
129+
end
130+
end
131+
132+
# Aborts the active transaction on the session, and calls
133+
# after_rollback callbacks on modified documents.
134+
#
135+
# @param [ Mongo::Session ] session Session on which
136+
# a transaction is started.
137+
def abort_transaction(session)
138+
session.abort_transaction
139+
Threaded.clear_modified_documents(session).each do |doc|
140+
doc.run_after_callbacks(:rollback)
141+
end
142+
end
106143
end
107144
end
108145
end

lib/mongoid/interceptable.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ module Interceptable
4343
# @api private
4444
define_model_callbacks :persist_parent
4545

46+
define_model_callbacks :commit, :rollback, only: :after
47+
4648
attr_accessor :before_callback_halted
4749
end
4850

@@ -115,8 +117,14 @@ def run_before_callbacks(*kinds)
115117
# end
116118
#
117119
# @param [ Symbol ] kind The type of callback to execute.
118-
# @param [ true | false ] with_children Flag specifies whether callbacks of embedded document should be run.
119-
def run_callbacks(kind, with_children: true, &block)
120+
# @param [ true | false ] with_children Flag specifies whether callbacks
121+
# of embedded document should be run.
122+
# @param [ Proc | nil ] skip_if If this proc returns true, the callbacks
123+
# will not be triggered, while the given block will be still called.
124+
def run_callbacks(kind, with_children: true, skip_if: nil, &block)
125+
if skip_if&.call
126+
return block&.call
127+
end
120128
if with_children
121129
cascadable_children(kind).each do |child|
122130
if child.run_callbacks(child_callback_type(kind, child), with_children: with_children) == false

lib/mongoid/persistable.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ def executing_atomically?
167167
def post_process_persist(result, options = {})
168168
post_persist unless result == false
169169
errors.clear unless performing_validations?(options)
170+
if in_transaction?
171+
Threaded.add_modified_document(_session, self)
172+
end
170173
true
171174
end
172175

lib/mongoid/persistable/creatable.rb

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,17 @@ def prepare_insert(options = {})
103103
raise Errors::ReadonlyDocument.new(self.class) if readonly? && !Mongoid.legacy_readonly
104104
return self if performing_validations?(options) &&
105105
invalid?(options[:context] || :create)
106-
run_callbacks(:save, with_children: false) do
107-
run_callbacks(:create, with_children: false) do
108-
run_callbacks(:persist_parent, with_children: false) do
109-
_mongoid_run_child_callbacks(:save) do
110-
_mongoid_run_child_callbacks(:create) do
111-
result = yield(self)
112-
if !result.is_a?(Document) || result.errors.empty?
113-
post_process_insert
114-
post_process_persist(result, options)
106+
run_callbacks(:commit, with_children: true, skip_if: -> { in_transaction? }) do
107+
run_callbacks(:save, with_children: false) do
108+
run_callbacks(:create, with_children: false) do
109+
run_callbacks(:persist_parent, with_children: false) do
110+
_mongoid_run_child_callbacks(:save) do
111+
_mongoid_run_child_callbacks(:create) do
112+
result = yield(self)
113+
if !result.is_a?(Document) || result.errors.empty?
114+
post_process_insert
115+
post_process_persist(result, options)
116+
end
115117
end
116118
end
117119
end

lib/mongoid/persistable/deletable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def notifying_parent?(options = {})
9999
# collection.find(atomic_selector).remove
100100
# end
101101
#
102-
# @return [ Object ] The result of the block.
102+
# @return [ true ] If the object was deleted successfully.
103103
def prepare_delete
104104
raise Errors::ReadonlyDocument.new(self.class) if readonly?
105105
yield(self)

lib/mongoid/persistable/destroyable.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ module Destroyable
1818
def destroy(options = nil)
1919
raise Errors::ReadonlyDocument.new(self.class) if readonly?
2020
self.flagged_for_destroy = true
21-
result = run_callbacks(:destroy) do
22-
if catch(:abort) { apply_destroy_dependencies! }
23-
delete(options || {})
24-
else
25-
false
21+
result = run_callbacks(:commit, skip_if: -> { in_transaction? }) do
22+
run_callbacks(:destroy) do
23+
if catch(:abort) { apply_destroy_dependencies! }
24+
delete(options || {}).tap do |res|
25+
if res && in_transaction?
26+
Threaded.add_modified_document(_session, self)
27+
end
28+
end
29+
else
30+
false
31+
end
2632
end
2733
end
2834
self.flagged_for_destroy = false

lib/mongoid/persistable/updatable.rb

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,17 @@ def prepare_update(options = {})
102102
process_flagged_destroys
103103
update_children = cascadable_children(:update)
104104
process_touch_option(options, update_children)
105-
run_callbacks(:save, with_children: false) do
106-
run_callbacks(:update, with_children: false) do
107-
run_callbacks(:persist_parent, with_children: false) do
108-
_mongoid_run_child_callbacks(:save) do
109-
_mongoid_run_child_callbacks(:update, children: update_children) do
110-
result = yield(self)
111-
self.previously_new_record = false
112-
post_process_persist(result, options)
113-
true
105+
run_callbacks(:commit, with_children: true, skip_if: -> { in_transaction? }) do
106+
run_callbacks(:save, with_children: false) do
107+
run_callbacks(:update, with_children: false) do
108+
run_callbacks(:persist_parent, with_children: false) do
109+
_mongoid_run_child_callbacks(:save) do
110+
_mongoid_run_child_callbacks(:update, children: update_children) do
111+
result = yield(self)
112+
self.previously_new_record = false
113+
post_process_persist(result, options)
114+
true
115+
end
114116
end
115117
end
116118
end

lib/mongoid/threaded.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ module Threaded
2929
# The key for the current thread's sessions.
3030
SESSIONS_KEY="[mongoid]:sessions"
3131

32+
# The key for storing documents modified inside transactions.
33+
MODIFIED_DOCUMENTS_KEY="[mongoid]:modified-documents"
34+
3235
extend self
3336

3437
# Begin entry into a named thread local stack.
@@ -353,9 +356,41 @@ def clear_session(client: nil)
353356
sessions.delete(client.object_id)&.end_session
354357
end
355358

359+
# Store a reference to the document that was modified inside a transaction
360+
# associated with the session.
361+
#
362+
# @param [ Mongo::Session ] session Session in scope of which the document
363+
# was modified.
364+
# @param [ Mongoid::Document ] document Mongoid document that was modified.
365+
def add_modified_document(session, document)
366+
if session&.in_transaction?
367+
modified_documents[session] << document
368+
end
369+
end
370+
371+
# Clears the set of modified documents for the given session, and return the
372+
# content of the set before the clearance.
373+
# @param [ Mongo::Session ] session Session for which the modified documents
374+
# set should be cleared.
375+
#
376+
# @return [ Set<Mongoid::Document> ] Collection of modified documents before
377+
# it was cleared.
378+
def clear_modified_documents(session)
379+
modified_documents[session].dup
380+
ensure
381+
modified_documents[session].clear
382+
end
383+
356384
# @api private
357385
def sessions
358386
Thread.current[SESSIONS_KEY] ||= {}
359387
end
388+
389+
# @api private
390+
def modified_documents
391+
Thread.current[MODIFIED_DOCUMENTS_KEY] ||= Hash.new do |h, k|
392+
h[k] = Set.new
393+
end
394+
end
360395
end
361396
end

0 commit comments

Comments
 (0)