activerecord-transactionable 0.1.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|