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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cdd81b65018bf33891cc544fe49c84e135e11cc6
4
- data.tar.gz: 05ef7ae436dfb6209334b5c34e7236983c1aea0d
3
+ metadata.gz: 838f13e51d720bccaec065d8c435ff17cfa6464e
4
+ data.tar.gz: 02a5eb9919b324c95a7d1cad51661aa7bac9ff87
5
5
  SHA512:
6
- metadata.gz: cec4d15f0088a1cc2027f16cb98917a98de1e8ef57cb83af99d61355e71d011841ba19457f10747eeb7846709b02a032d3771aecb862782da171798aface8b78
7
- data.tar.gz: d766492135b7c457c0d2152061f457c8fe5fd4e030ec324b829fdb63c3d8e70c0e5396764d5324a367d63af29f6e9ea6dd3eb45399e94b526a8a64bf6401c375
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
- DEFAULT_ERRORS_TO_HANDLE = [ActiveRecord::RecordInvalid]
17
- DEFAULT_ERRORS_WHICH_PREPARE_ERRORS_ON_SELF = [ActiveRecord::RecordInvalid]
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 args[:lock]
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
- error_handler(object: object, **args) do
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
- error_handler(object: object, **args) do
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 args[:lock]
44
- ActiveRecord::Base.transaction do
45
- error_handler(object: object, **args) do
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
- private
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 error_handler(object: nil, **args)
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(DEFAULT_ERRORS_TO_HANDLE)
60
- prepared_errors.concat(DEFAULT_ERRORS_WHICH_PREPARE_ERRORS_ON_SELF)
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
- yield
69
- true # <= make the return value meaningful. Meaning: transaction succeeded, no errors raised
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
- transaction_error_logger(object: nil, error: error, additional_message: nil)
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
- transaction_error_logger(object: object, error: error, additional_message: nil)
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
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Transactionable
3
- VERSION = "0.1.3"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  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.1.3
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-10 00:00:00.000000000 Z
11
+ date: 2017-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel