-
Notifications
You must be signed in to change notification settings - Fork 24
/
Copy pathafter_commit_everywhere.rb
175 lines (157 loc) · 6.98 KB
/
after_commit_everywhere.rb
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
# frozen_string_literal: true
require "active_record"
require "active_support/core_ext/module/delegation"
require "after_commit_everywhere/version"
require "after_commit_everywhere/wrap"
# Module allowing to use ActiveRecord transactional callbacks outside of
# ActiveRecord models, literally everywhere in your application.
#
# Include it to your classes (e.g. your base service object class or whatever)
module AfterCommitEverywhere
class NotInTransaction < RuntimeError; end
delegate :after_commit, :before_commit, :after_rollback, to: AfterCommitEverywhere
delegate :in_transaction?, :in_transaction, to: AfterCommitEverywhere
# Causes {before_commit} and {after_commit} to raise an exception when
# called outside a transaction.
RAISE = :raise
# Causes {before_commit} and {after_commit} to execute the given callback
# immediately when called outside a transaction.
EXECUTE = :execute
# Causes {before_commit} and {after_commit} to log a warning before calling
# the given callback immediately when called outside a transaction.
WARN_AND_EXECUTE = :warn_and_execute
class << self
# Runs +callback+ after successful commit of outermost transaction for
# database +connection+.
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection to operate in. Defaults to +ActiveRecord::Base.connection+
# @param without_tx [Symbol] Determines the behavior of this function when
# called without an open transaction.
#
# Must be one of: {RAISE}, {EXECUTE}, or {WARN_AND_EXECUTE}.
#
# @param callback [#call] Callback to be executed
# @return void
def after_commit(
prepend: false,
connection: nil,
without_tx: EXECUTE,
&callback
)
register_callback(
prepend: prepend,
connection: connection,
name: __method__,
callback: callback,
without_tx: without_tx,
)
end
# Runs +callback+ before committing of outermost transaction for +connection+.
#
# Available only since Ruby on Rails 5.0. See https://github.com/rails/rails/pull/18936
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection to operate in. Defaults to +ActiveRecord::Base.connection+
# @param without_tx [Symbol] Determines the behavior of this function when
# called without an open transaction.
#
# Must be one of: {RAISE}, {EXECUTE}, or {WARN_AND_EXECUTE}.
#
# @param callback [#call] Callback to be executed
# @return void
def before_commit(
prepend: false,
connection: nil,
without_tx: WARN_AND_EXECUTE,
&callback
)
if ActiveRecord::VERSION::MAJOR < 5
raise NotImplementedError, "#{__method__} works only with Rails 5.0+"
end
register_callback(
prepend: prepend,
connection: connection,
name: __method__,
callback: callback,
without_tx: without_tx,
)
end
# Runs +callback+ after rolling back of transaction or savepoint (if declared
# in nested transaction) for database +connection+.
#
# Caveat: do not raise +ActivRecord::Rollback+ in nested transaction block!
# See http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection to operate in. Defaults to +ActiveRecord::Base.connection+
# @param callback [#call] Callback to be executed
# @return void
# @raise [NotInTransaction] if called outside transaction.
def after_rollback(prepend: false, connection: nil, &callback)
register_callback(
prepend: prepend,
connection: connection,
name: __method__,
callback: callback,
without_tx: RAISE,
)
end
# @api private
def register_callback(prepend:, connection: nil, name:, without_tx:, callback:)
raise ArgumentError, "Provide callback to #{name}" unless callback
unless in_transaction?(connection)
case without_tx
when WARN_AND_EXECUTE
warn "#{name}: No transaction open. Executing callback immediately."
return callback.call
when EXECUTE
return callback.call
when RAISE
raise NotInTransaction, "#{name} is useless outside transaction"
else
raise ArgumentError, "Invalid \"without_tx\": \"#{without_tx}\""
end
end
connection ||= default_connection
wrap = Wrap.new(connection: connection, "#{name}": callback)
if prepend
# Hacking ActiveRecord's transaction internals to prepend our callback
# See https://github.com/rails/rails/blob/f0d433bb46ac233ec7fd7fae48f458978908d905/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L148-L156
records = connection.current_transaction.instance_variable_get(:@records)
records = connection.current_transaction.instance_variable_set(:@records, []) if records.nil?
records.unshift(wrap)
else
connection.add_transaction_record(wrap)
end
end
# Helper method to determine whether we're currently in transaction or not
def in_transaction?(connection = nil)
# Don't establish new connection if not connected: we apparently not in transaction
return false unless connection || ActiveRecord::Base.connection_pool.active_connection?
connection ||= default_connection
# service transactions (tests and database_cleaner) are not joinable
connection.transaction_open? && connection.current_transaction.joinable?
end
# Makes sure the provided block runs in a transaction. If we are not currently in a transaction, a new transaction is started.
#
# It mimics the ActiveRecord's +transaction+ method's API and actually uses it under the hood.
#
# However, the main difference is that it doesn't swallow +ActiveRecord::Rollback+ exception in case when there is no transaction open.
#
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-transaction
#
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection to operate in. Defaults to +ActiveRecord::Base.connection+
# @param requires_new [Boolean] Forces creation of new subtransaction (savepoint) even if transaction is already opened.
# @param new_tx_options [Hash<Symbol, void>] Options to be passed to +connection.transaction+ on new transaction creation
# @return void
def in_transaction(connection = default_connection, requires_new: false, **new_tx_options)
if in_transaction?(connection) && !requires_new
yield
else
connection.transaction(requires_new: requires_new, **new_tx_options) { yield }
end
end
private
def default_connection
ActiveRecord::Base.connection
end
end
end