activerecord-transactionable 2.0.3 → 2.0.4
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/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
|