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 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