activerecord-transactionable 2.0.3 → 2.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -4
- data/lib/activerecord/transactionable.rb +28 -18
- data/lib/activerecord/transactionable/result.rb +9 -7
- data/lib/activerecord/transactionable/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40e310ce0f07066a58c6b6a813b79c8f5dcfda4c63775e0eba78becbea0f156a
|
4
|
+
data.tar.gz: 0d583ee211e147bb1d14c514bdb3497091433fa00733fdefcfc4571446425e5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 428e31d3bee1d6c842d27b6a17d6143d1ad00c781e9663e4ea6ff9da5e4b8f1f584d2147f2f3c69330de1e31defdd2722a2015e0dc8f4a11718b6ba4cce4cefd
|
7
|
+
data.tar.gz: e14d70b5d072b34303cce8f38f7ddacc13282bd6f0d45e05baa6789d1f81c248fbd170010943d2a09d03c9047fc361d72652304c2af0fdf4a208ed509eaa7a0d
|
data/README.md
CHANGED
@@ -134,18 +134,19 @@ transaction_result = @client.transaction_wrapper(lock: true) do
|
|
134
134
|
if transaction_result.success?
|
135
135
|
render :show, locals: { client: @client }, status: :ok
|
136
136
|
else
|
137
|
-
# Something prevented update
|
138
|
-
render json: @client.errors, status: :unprocessable_entity
|
137
|
+
# Something prevented update, transaction_result.to_h will have all the available details
|
138
|
+
render json: { record_errors: @client.errors, transaction_result: transaction_result.to_h }, status: :unprocessable_entity
|
139
139
|
end
|
140
140
|
```
|
141
141
|
|
142
142
|
## Find or create
|
143
143
|
|
144
|
-
NOTE: The `is_retry` is passed to the block by the gem, and indicates whether the block is running for the first time or the second time.
|
144
|
+
NOTE: The `is_retry` is passed to the block by the gem, and indicates whether the block is running for the first time or the second, or nth, time.
|
145
145
|
The block will never be retried more than once.
|
146
146
|
|
147
147
|
```ruby
|
148
|
-
Car.transaction_wrapper(outside_retriable_errors: ActivRecord::RecordNotFound) do |is_retry|
|
148
|
+
Car.transaction_wrapper(outside_retriable_errors: ActivRecord::RecordNotFound, outside_num_retry_attempts: 3) do |is_retry|
|
149
|
+
# is_retry will be falsey on first attempt, thereafter will be the integer number of the attempt
|
149
150
|
if is_retry
|
150
151
|
Car.create!(vin: vin)
|
151
152
|
else
|
@@ -161,6 +162,7 @@ The block will never be retried more than once.
|
|
161
162
|
|
162
163
|
```ruby
|
163
164
|
Car.transaction_wrapper(outside_retriable_errors: ActivRecord::RecordNotUnique) do |is_retry|
|
165
|
+
# is_retry will be falsey on first attempt, thereafter will be the integer number of the attempt
|
164
166
|
if is_retry
|
165
167
|
Car.find_by!(vin: vin)
|
166
168
|
else
|
@@ -15,6 +15,7 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
15
15
|
module Transactionable
|
16
16
|
extend ActiveSupport::Concern
|
17
17
|
|
18
|
+
DEFAULT_NUM_RETRY_ATTEMPTS = 2
|
18
19
|
DEFAULT_ERRORS_TO_HANDLE_INSIDE_TRANSACTION = [].freeze
|
19
20
|
DEFAULT_ERRORS_PREPARE_ON_SELF_INSIDE = [].freeze
|
20
21
|
DEFAULT_ERRORS_TO_HANDLE_OUTSIDE_TRANSACTION = [ActiveRecord::RecordInvalid].freeze
|
@@ -38,12 +39,14 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
38
39
|
prepared_errors
|
39
40
|
retriable_errors
|
40
41
|
reraisable_errors
|
42
|
+
num_retry_attempts
|
41
43
|
].freeze
|
42
44
|
OUTSIDE_TRANSACTION_ERROR_HANDLERS = %i[
|
43
45
|
outside_rescued_errors
|
44
46
|
outside_prepared_errors
|
45
47
|
outside_retriable_errors
|
46
48
|
outside_reraisable_errors
|
49
|
+
outside_num_retry_attempts
|
47
50
|
].freeze
|
48
51
|
INSIDE_CONTEXT = 'inside'.freeze
|
49
52
|
OUTSIDE_CONTEXT = 'outside'.freeze
|
@@ -124,11 +127,12 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
124
127
|
prepared_errors = Array(args[:prepared_errors])
|
125
128
|
retriable_errors = Array(args[:retriable_errors])
|
126
129
|
reraisable_errors = Array(args[:reraisable_errors])
|
130
|
+
num_retry_attempts = args[:num_retry_attempts] ? args[:num_retry_attempts].to_i : DEFAULT_NUM_RETRY_ATTEMPTS
|
127
131
|
rescued_errors.concat(DEFAULT_ERRORS_TO_HANDLE_INSIDE_TRANSACTION)
|
128
132
|
prepared_errors.concat(DEFAULT_ERRORS_PREPARE_ON_SELF_INSIDE)
|
129
133
|
already_been_added_to_self, needing_added_to_self = rescued_errors.partition { |error_class| prepared_errors.include?(error_class) }
|
130
134
|
local_context = INSIDE_CONTEXT
|
131
|
-
run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self) do |is_retry|
|
135
|
+
run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self, num_retry_attempts) do |is_retry|
|
132
136
|
yield is_retry
|
133
137
|
end
|
134
138
|
end
|
@@ -138,19 +142,22 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
138
142
|
prepared_errors = Array(args[:outside_prepared_errors])
|
139
143
|
retriable_errors = Array(args[:outside_retriable_errors])
|
140
144
|
reraisable_errors = Array(args[:outside_reraisable_errors])
|
145
|
+
num_retry_attempts = args[:outside_num_retry_attempts] ? args[:outside_num_retry_attempts].to_i : DEFAULT_NUM_RETRY_ATTEMPTS
|
141
146
|
rescued_errors.concat(DEFAULT_ERRORS_TO_HANDLE_OUTSIDE_TRANSACTION)
|
142
147
|
prepared_errors.concat(DEFAULT_ERRORS_PREPARE_ON_SELF_OUTSIDE)
|
143
148
|
already_been_added_to_self, needing_added_to_self = rescued_errors.partition { |error_class| prepared_errors.include?(error_class) }
|
144
149
|
local_context = OUTSIDE_CONTEXT
|
145
|
-
run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self) do |is_retry|
|
150
|
+
run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self, num_retry_attempts) do |is_retry|
|
146
151
|
yield is_retry
|
147
152
|
end
|
148
153
|
end
|
149
154
|
|
150
|
-
def run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self)
|
155
|
+
def run_block_with_retry(object, local_context, transaction_open, retriable_errors, reraisable_errors, already_been_added_to_self, needing_added_to_self, num_retry_attempts)
|
156
|
+
attempt = 0
|
151
157
|
re_try = false
|
152
158
|
begin
|
153
|
-
|
159
|
+
attempt += 1
|
160
|
+
# If the block we yield to here raises an error that is not caught below the `re_try = true` will not get hit.
|
154
161
|
# If the error is rescued higher up, like where the transaction in active record
|
155
162
|
# rescues ActiveRecord::Rollback without re-raising, then transaction_wrapper will return nil
|
156
163
|
# If the error is not rescued higher up the error will continue to bubble
|
@@ -159,54 +166,57 @@ module Activerecord # Note lowercase "r" in Activerecord (different namespace th
|
|
159
166
|
# We pass the retry state along to yield so that the code implementing
|
160
167
|
# the transaction_wrapper can switch behavior on a retry
|
161
168
|
# (e.g. create => find)
|
162
|
-
result = yield re_try
|
169
|
+
result = yield (re_try == false ? re_try : attempt)
|
163
170
|
# When in the outside context we need to preserve the inside result so it bubbles up unmolested with the "meaningful" result of the transaction.
|
164
171
|
if result.is_a?(Activerecord::Transactionable::Result)
|
165
172
|
result # <= preserve the meaningful return value
|
166
173
|
else
|
167
|
-
Activerecord::Transactionable::Result.new(true, context: local_context, transaction_open: transaction_open) # <= make the return value meaningful. Meaning: transaction succeeded, no errors raised
|
174
|
+
Activerecord::Transactionable::Result.new(true, context: local_context, attempt: attempt, transaction_open: transaction_open) # <= make the return value meaningful. Meaning: transaction succeeded, no errors raised
|
168
175
|
end
|
169
176
|
rescue *reraisable_errors => error
|
170
177
|
# This has highest precedence because raising is the most critical functionality of a raised error to keep
|
171
178
|
# if that is in the intended behavior, and this way a specific child of StandardError can be reraised while
|
172
179
|
# the parent can still be caught and added to self.errors
|
173
180
|
# Also adds the error to the object if there is an object.
|
174
|
-
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context} re-raising!]")
|
181
|
+
transaction_error_logger(object: object, error: error, result: nil, attempt: attempt, add_to: nil, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context} re-raising!]")
|
175
182
|
raise error
|
176
183
|
rescue *retriable_errors => error
|
177
184
|
# This will re-run the begin block above
|
178
185
|
# WARNING: If the same error keeps getting thrown this would infinitely recurse!
|
179
186
|
# To avoid the infinite recursion, we track the retry state
|
180
|
-
if
|
181
|
-
|
182
|
-
|
187
|
+
if attempt >= num_retry_attempts
|
188
|
+
result = Activerecord::Transactionable::Result.new(false, context: local_context, transaction_open: transaction_open, error: error, attempt: attempt, type: 'retriable') # <= make the return value meaningful. Meaning is: transaction failed after <attempt> attempts
|
189
|
+
transaction_error_logger(object: object, error: error, result: result, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
|
190
|
+
result
|
183
191
|
else
|
184
192
|
re_try = true
|
185
193
|
# Not adding error to base when retrying, because otherwise the added error may
|
186
194
|
# prevent the subsequent save from working, in a catch-22
|
187
|
-
transaction_error_logger(object: object, error: error, add_to: nil, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}
|
195
|
+
transaction_error_logger(object: object, error: error, result: nil, attempt: attempt, add_to: nil, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
|
188
196
|
retry
|
189
197
|
end
|
190
198
|
rescue *already_been_added_to_self => error
|
191
199
|
# ActiveRecord::RecordInvalid, when done correctly, will have already added the error to object.
|
192
|
-
|
193
|
-
|
200
|
+
result = Activerecord::Transactionable::Result.new(false, context: local_context, transaction_open: transaction_open, error: error, attempt: attempt, type: 'already_added') # <= make the return value meaningful. Meaning is: transaction failed
|
201
|
+
transaction_error_logger(object: nil, error: error, result: result, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
|
202
|
+
result
|
194
203
|
rescue *needing_added_to_self => error
|
195
|
-
|
196
|
-
|
204
|
+
result = Activerecord::Transactionable::Result.new(false, context: local_context, transaction_open: transaction_open, error: error, attempt: attempt, type: 'needing_added') # <= make the return value meaningful. Meaning is: transaction failed
|
205
|
+
transaction_error_logger(object: object, error: error, result: result, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
|
206
|
+
result
|
197
207
|
end
|
198
208
|
end
|
199
209
|
|
200
|
-
def transaction_error_logger(object:, error:, add_to: :base, additional_message: nil)
|
210
|
+
def transaction_error_logger(object:, error:, result:, attempt: nil, add_to: :base, additional_message: nil, **opts)
|
201
211
|
# Ruby arguments, like object, are passed by reference,
|
202
212
|
# so this update to errors will be available to the caller
|
203
213
|
if object.nil?
|
204
214
|
# when a transaction wraps a bunch of CRUD actions,
|
205
215
|
# the specific record that caused the ActiveRecord::RecordInvalid error may be out of scope
|
206
216
|
# Ideally you would rewrite the caller to call transaction_wrapper on a single record (even if updates happen on other records)
|
207
|
-
logger.error("[#{self}.transaction_wrapper] #{error.class}: #{error.message}#{additional_message}")
|
217
|
+
logger.error("[#{self}.transaction_wrapper] #{error.class}: #{error.message}#{additional_message}[#{attempt || (result && result.to_h[:attempt])}]")
|
208
218
|
else
|
209
|
-
logger.error("[#{self}.transaction_wrapper] On #{object.class} #{error.class}: #{error.message}#{additional_message}")
|
219
|
+
logger.error("[#{self}.transaction_wrapper] On #{object.class} #{error.class}: #{error.message}#{additional_message}[#{attempt || (result && result.to_h[:attempt])}]")
|
210
220
|
object.errors.add(add_to, error.message) unless add_to.nil?
|
211
221
|
end
|
212
222
|
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
module Activerecord
|
2
2
|
module Transactionable
|
3
3
|
class Result
|
4
|
-
attr_reader :value, :result, :error, :type, :context, :nested
|
5
|
-
def initialize(value, context:, transaction_open:, error: nil, type: nil)
|
4
|
+
attr_reader :value, :result, :error, :type, :context, :nested, :attempt
|
5
|
+
def initialize(value, context:, transaction_open:, attempt:, error: nil, type: nil)
|
6
6
|
@value = value
|
7
7
|
@result = fail? ? 'fail' : 'success'
|
8
8
|
@context = context
|
9
9
|
@nested = transaction_open ? true : false
|
10
|
+
@attempt = attempt
|
10
11
|
@error = error
|
11
12
|
@type = type
|
12
13
|
end
|
@@ -19,22 +20,23 @@ module Activerecord
|
|
19
20
|
value == true
|
20
21
|
end
|
21
22
|
|
22
|
-
def to_h
|
23
|
+
def to_h(skip_error: nil)
|
23
24
|
diagnostic_data = {
|
24
25
|
result: result,
|
25
26
|
type: type,
|
26
27
|
context: context,
|
27
|
-
nested: nested
|
28
|
+
nested: nested,
|
29
|
+
attempt: attempt,
|
28
30
|
}
|
29
31
|
diagnostic_data.merge!(
|
30
32
|
error: error.class.to_s,
|
31
33
|
message: error.message,
|
32
|
-
) if error
|
34
|
+
) if !skip_error && error
|
33
35
|
diagnostic_data
|
34
36
|
end
|
35
37
|
|
36
|
-
def to_s
|
37
|
-
to_h.to_s
|
38
|
+
def to_s(skip_error: nil)
|
39
|
+
to_h(skip_error: skip_error).to_s
|
38
40
|
end
|
39
41
|
end
|
40
42
|
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: 2.0.
|
4
|
+
version: 2.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Boling
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-09-
|
11
|
+
date: 2018-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|