activerecord-transactionable 0.1.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/activerecord/transactionable.rb +141 -23
- data/lib/activerecord/transactionable/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 838f13e51d720bccaec065d8c435ff17cfa6464e
|
4
|
+
data.tar.gz: 02a5eb9919b324c95a7d1cad51661aa7bac9ff87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4af2e50720c55e48e3a2859048d219bc4d6284564112fa27cc699a69456fc92f198d5c78bb474441ce41de36499e01a50c2b07b5355a16e2676fb8312e3b1a2b
|
7
|
+
data.tar.gz: 2e92369926fc990d3ea2edf7c6faab25e6934dd299ad75d7012a2e2d0eb32c4949bb4c56ebcd9221168ea4e7c136c858abbdf1e496f5c07e45d70a15e6d728e6
|
@@ -13,8 +13,37 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
13
13
|
module Transactionable
|
14
14
|
extend ActiveSupport::Concern
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
DEFAULT_ERRORS_TO_HANDLE_INSIDE_TRANSACTION = []
|
17
|
+
DEFAULT_ERRORS_PREPARE_ON_SELF_INSIDE = []
|
18
|
+
DEFAULT_ERRORS_TO_HANDLE_OUTSIDE_TRANSACTION = [ ActiveRecord::RecordInvalid ]
|
19
|
+
DEFAULT_ERRORS_PREPARE_ON_SELF_OUTSIDE = [ ActiveRecord::RecordInvalid ]
|
20
|
+
# These errors (and possibly others) will invalidate the transaction (on PostgreSQL and possibly other databases).
|
21
|
+
# This means that if you did rescue them inside a transaction (or a nested transaction) all subsequent queries would fail.
|
22
|
+
ERRORS_TO_DISALLOW_INSIDE_TRANSACTION = [
|
23
|
+
ActiveRecord::RecordInvalid,
|
24
|
+
ActiveRecord::StatementInvalid,
|
25
|
+
ActiveRecord::RecordNotUnique
|
26
|
+
].freeze
|
27
|
+
# http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-transaction
|
28
|
+
TRANSACTION_METHOD_ARG_NAMES = [
|
29
|
+
:requires_new,
|
30
|
+
:isolation,
|
31
|
+
:joinable
|
32
|
+
].freeze
|
33
|
+
INSIDE_TRANSACTION_ERROR_HANDLERS = [
|
34
|
+
:rescued_errors,
|
35
|
+
:prepared_errors,
|
36
|
+
:retriable_errors,
|
37
|
+
:reraisable_errors
|
38
|
+
].freeze
|
39
|
+
OUTSIDE_TRANSACTION_ERROR_HANDLERS = [
|
40
|
+
:outside_rescued_errors,
|
41
|
+
:outside_prepared_errors,
|
42
|
+
:outside_retriable_errors,
|
43
|
+
:outside_reraisable_errors
|
44
|
+
].freeze
|
45
|
+
INSIDE_CONTEXT = "inside".freeze
|
46
|
+
OUTSIDE_CONTEXT = "outside".freeze
|
18
47
|
|
19
48
|
def transaction_wrapper(**args)
|
20
49
|
self.class.transaction_wrapper(object: self, **args) do
|
@@ -24,78 +53,166 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
24
53
|
|
25
54
|
module ClassMethods
|
26
55
|
def transaction_wrapper(object: nil, **args)
|
56
|
+
lock = args.delete(:lock)
|
57
|
+
inside_args = extract_args(args, INSIDE_TRANSACTION_ERROR_HANDLERS)
|
58
|
+
outside_args = extract_args(args, OUTSIDE_TRANSACTION_ERROR_HANDLERS)
|
59
|
+
transaction_open = ActiveRecord::Base.connection.transaction_open?
|
60
|
+
raise ArgumentError, "#{self} does not know how to handle arguments: #{args.keys.inspect}" unless args.keys.empty?
|
61
|
+
if ERRORS_TO_DISALLOW_INSIDE_TRANSACTION.detect { |error| inside_args.values.include?(error) }
|
62
|
+
raise ArgumentError, "#{self} should not rescue #{ERRORS_TO_DISALLOW_INSIDE_TRANSACTION.inspect} inside a transaction: #{args.keys.inspect}"
|
63
|
+
end
|
64
|
+
transaction_args = extract_args(args, TRANSACTION_METHOD_ARG_NAMES)
|
65
|
+
if transaction_open
|
66
|
+
unless transaction_args[:require_new]
|
67
|
+
transaction_args[:require_new] = true
|
68
|
+
logger.warn("[#{self}.transaction_wrapper] Opening a nested transaction. Setting require_new: true")
|
69
|
+
else
|
70
|
+
logger.debug("[#{self}.transaction_wrapper] Will start a nested transaction.")
|
71
|
+
end
|
72
|
+
run_inside_transaction_block(transaction_args: transaction_args, inside_args: inside_args, lock: lock, transaction_open: transaction_open, object: object) do
|
73
|
+
yield
|
74
|
+
end
|
75
|
+
else
|
76
|
+
puts "what 1 #{__method__}, transaction_open: #{transaction_open}"
|
77
|
+
error_handler_outside_transaction(object: object, transaction_open: transaction_open, **outside_args) do
|
78
|
+
puts "what 1.2 #{__method__}, transaction_open: #{transaction_open}"
|
79
|
+
run_inside_transaction_block(transaction_args: transaction_args, inside_args: inside_args, lock: lock, transaction_open: transaction_open, object: object) do
|
80
|
+
puts "what 1.3 #{__method__}, transaction_open: #{transaction_open}"
|
81
|
+
yield
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def run_inside_transaction_block(transaction_args:, inside_args:, lock:, transaction_open:, object: nil)
|
27
90
|
if object
|
28
|
-
if
|
29
|
-
# Note with_lock will reload object!
|
91
|
+
if lock
|
92
|
+
# Note: with_lock will reload object!
|
93
|
+
# Note: with_lock does not accept arguments like transaction does.
|
30
94
|
object.with_lock do
|
31
|
-
|
95
|
+
puts "what 4.a #{__method__}, transaction_open: #{transaction_open}"
|
96
|
+
error_handler_inside_transaction(object: object, transaction_open: transaction_open, **inside_args) do
|
97
|
+
puts "what 4.a.1 #{__method__}, transaction_open: #{transaction_open}"
|
32
98
|
yield
|
33
99
|
end
|
34
100
|
end
|
35
101
|
else
|
36
|
-
object.transaction do
|
37
|
-
|
102
|
+
object.transaction(**transaction_args) do
|
103
|
+
puts "what 4.b #{__method__}, transaction_open: #{transaction_open}"
|
104
|
+
error_handler_inside_transaction(object: object, transaction_open: transaction_open, **inside_args) do
|
105
|
+
puts "what 4.b.1 #{__method__}, transaction_open: #{transaction_open}"
|
38
106
|
yield
|
39
107
|
end
|
40
108
|
end
|
41
109
|
end
|
42
110
|
else
|
43
|
-
raise ArgumentError, "No object to lock!" if
|
44
|
-
ActiveRecord::Base.transaction do
|
45
|
-
|
111
|
+
raise ArgumentError, "No object to lock!" if lock
|
112
|
+
ActiveRecord::Base.transaction(**transaction_args) do
|
113
|
+
puts "what 4.c #{__method__}, transaction_open: #{transaction_open}"
|
114
|
+
error_handler_inside_transaction(object: object, transaction_open: transaction_open, **inside_args) do
|
115
|
+
puts "what 4.c.1 #{__method__}, transaction_open: #{transaction_open}"
|
46
116
|
yield
|
47
117
|
end
|
48
118
|
end
|
49
119
|
end
|
50
120
|
end
|
51
121
|
|
52
|
-
|
122
|
+
# returns a hash of the arguments to the ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction method
|
123
|
+
def extract_args(args, arg_names)
|
124
|
+
arg_names.each_with_object({}) do |key, hash|
|
125
|
+
hash[key] = args.delete(key)
|
126
|
+
end
|
127
|
+
end
|
53
128
|
|
54
|
-
def
|
129
|
+
def error_handler_inside_transaction(object: nil, transaction_open:, **args)
|
130
|
+
puts "what 5 #{__method__}, transaction_open: #{transaction_open}"
|
55
131
|
rescued_errors = Array(args[:rescued_errors])
|
56
132
|
prepared_errors = Array(args[:prepared_errors])
|
57
133
|
retriable_errors = Array(args[:retriable_errors])
|
58
134
|
reraisable_errors = Array(args[:reraisable_errors])
|
59
|
-
rescued_errors.concat(
|
60
|
-
prepared_errors.concat(
|
135
|
+
rescued_errors.concat(DEFAULT_ERRORS_TO_HANDLE_INSIDE_TRANSACTION)
|
136
|
+
prepared_errors.concat(DEFAULT_ERRORS_PREPARE_ON_SELF_INSIDE)
|
61
137
|
already_been_added_to_self, needing_added_to_self = rescued_errors.partition {|error_class| prepared_errors.include?(error_class)}
|
138
|
+
local_context = INSIDE_CONTEXT
|
139
|
+
run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self) do
|
140
|
+
puts "what 5.1 #{__method__}, transaction_open: #{transaction_open}"
|
141
|
+
yield
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def error_handler_outside_transaction(object: nil, transaction_open:, **args)
|
146
|
+
puts "what 2.1 #{__method__}, transaction_open: #{transaction_open}"
|
147
|
+
rescued_errors = Array(args[:outside_rescued_errors])
|
148
|
+
prepared_errors = Array(args[:outside_prepared_errors])
|
149
|
+
retriable_errors = Array(args[:outside_retriable_errors])
|
150
|
+
reraisable_errors = Array(args[:outside_reraisable_errors])
|
151
|
+
rescued_errors.concat(DEFAULT_ERRORS_TO_HANDLE_OUTSIDE_TRANSACTION)
|
152
|
+
prepared_errors.concat(DEFAULT_ERRORS_PREPARE_ON_SELF_OUTSIDE)
|
153
|
+
already_been_added_to_self, needing_added_to_self = rescued_errors.partition {|error_class| prepared_errors.include?(error_class)}
|
154
|
+
local_context = OUTSIDE_CONTEXT
|
155
|
+
run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self) do
|
156
|
+
puts "what 2.2 #{__method__}, transaction_open: #{transaction_open}"
|
157
|
+
yield
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self)
|
162
|
+
puts "what 3.1 #{__method__}: local_context: #{local_context}, transaction_open: #{transaction_open}"
|
62
163
|
re_try = false
|
63
|
-
begin
|
164
|
+
result = begin
|
64
165
|
# If the block we yield to here raises an error that is not caught below the `true` will not get hit.
|
65
166
|
# If the error is rescued higher up, like where the transaction in active record
|
66
167
|
# rescues ActiveRecord::Rollback without re-raising, then transaction_wrapper will return nil
|
67
168
|
# If the error is not rescued higher up the error will continue to bubble
|
68
|
-
|
69
|
-
|
169
|
+
# If we were already inside a transaction, such that this one is nested,
|
170
|
+
# then the result of the yield is what we want to return, to preserve the innermost result
|
171
|
+
result = yield
|
172
|
+
# When in the outside context we need to preserve the inside result so it bubles up unmolested with the "meaningful" result of the transaction.
|
173
|
+
if transaction_open || local_context == OUTSIDE_CONTEXT
|
174
|
+
puts "what 3.2.a preserving: #{result.inspect}, local_context: #{local_context}, transaction_open: #{transaction_open}"
|
175
|
+
result # <= preserve the meaningful return value. Meaning: transaction succeeded, no errors raised
|
176
|
+
else
|
177
|
+
puts "what 3.2.b no errors! local_context: #{local_context}, transaction_open: #{transaction_open}"
|
178
|
+
true # <= make the return value meaningful. Meaning: transaction succeeded, no errors raised
|
179
|
+
end
|
70
180
|
rescue *reraisable_errors => error
|
71
181
|
# This has highest precedence because raising is the most critical functionality of a raised error to keep
|
72
182
|
# if that is in the intended behavior, and this way a specific child of StandardError can be reraised while
|
73
183
|
# the parent can still be caught and added to self.errors
|
74
184
|
# Also adds the error to the object if there is an object.
|
75
|
-
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [re-raising!]")
|
185
|
+
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context} re-raising!]")
|
186
|
+
puts "what 3.2.c reraisable local_context: #{local_context}, transaction_open: #{transaction_open}"
|
76
187
|
raise error
|
77
188
|
rescue *retriable_errors => error
|
78
189
|
# This will re-run the begin block above
|
79
190
|
# WARNING: If the same error keeps getting thrown this would infinitely recurse!
|
80
191
|
# To avoid the infinite recursion, we track the retry state
|
81
192
|
if re_try
|
82
|
-
transaction_error_logger(object: object, error: error, additional_message: " [2nd attempt]")
|
193
|
+
transaction_error_logger(object: object, error: error, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context} 2nd attempt]")
|
194
|
+
puts "what 3.2.e post-retry local_context: #{local_context}, transaction_open: #{transaction_open}"
|
83
195
|
false # <= make the return value meaningful. Meaning is: transaction failed after two attempts
|
84
196
|
else
|
85
197
|
re_try = true
|
86
198
|
# Not adding error to base when retrying, because otherwise the added error may
|
87
199
|
# prevent the subsequent save from working, in a catch-22
|
88
|
-
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [1st attempt]")
|
200
|
+
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context} 1st attempt]")
|
201
|
+
puts "what 3.2.d pre-retry local_context: #{local_context}, transaction_open: #{transaction_open}"
|
89
202
|
retry
|
90
203
|
end
|
91
204
|
rescue *already_been_added_to_self => error
|
92
205
|
# ActiveRecord::RecordInvalid, when done correctly, will have already added the error to object.
|
93
|
-
|
206
|
+
puts "what 3.2.f already: local_context: #{local_context}, transaction_open: #{transaction_open}"
|
207
|
+
transaction_error_logger(object: nil, error: error, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
|
94
208
|
false # <= make the return value meaningful. Meaning is: transaction failed
|
95
209
|
rescue *needing_added_to_self => error
|
96
|
-
|
210
|
+
puts "what 3.2.g needing: local_context: #{local_context}, transaction_open: #{transaction_open}"
|
211
|
+
transaction_error_logger(object: object, error: error, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
|
97
212
|
false # <= make the return value meaningful. Meaning is: transaction failed
|
98
213
|
end
|
214
|
+
puts "what 3.3 result: #{result.inspect}"
|
215
|
+
result
|
99
216
|
end
|
100
217
|
|
101
218
|
def transaction_error_logger(object:, error:, add_to: :base, additional_message: nil)
|
@@ -106,12 +223,13 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
106
223
|
# the specific record that caused the ActiveRecord::RecordInvalid error may be out of scope
|
107
224
|
# Ideally you would rewrite the caller to call transaction_wrapper on a single record (even if updates happen on other records)
|
108
225
|
logger.error("[#{self}.transaction_wrapper] #{error.class}: #{error.message}#{additional_message}")
|
226
|
+
puts("[#{self}.transaction_wrapper] #{error.class}: #{error.message}#{additional_message}")
|
109
227
|
else
|
110
228
|
logger.error("[#{self}.transaction_wrapper] On #{object.class} #{error.class}: #{error.message}#{additional_message}")
|
229
|
+
puts("[#{self}.transaction_wrapper] On #{object.class} #{error.class}: #{error.message}#{additional_message}")
|
111
230
|
object.errors.add(add_to, error.message) unless add_to.nil?
|
112
231
|
end
|
113
232
|
end
|
114
233
|
end
|
115
|
-
|
116
234
|
end
|
117
235
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-transactionable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Boling
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-09-
|
11
|
+
date: 2017-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|