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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c72bcd7d394827cc75722f18b5c963b202553a16a5bce5be6163a68f43e9706a
4
- data.tar.gz: e55157daee5fba12b52c21db6296895bc794385ad6ee64ce5b10a5ad3c7ca036
3
+ metadata.gz: 40e310ce0f07066a58c6b6a813b79c8f5dcfda4c63775e0eba78becbea0f156a
4
+ data.tar.gz: 0d583ee211e147bb1d14c514bdb3497091433fa00733fdefcfc4571446425e5b
5
5
  SHA512:
6
- metadata.gz: ce66e65e7cc433cb3c1ce7137a718a295356ebc21b04a827288f040d4075d145af80ef0d272e7eee0638a69afd761d58ae12e8a7e93cd23c710e8313130e8c14
7
- data.tar.gz: 2760e001a9905e5ce35e0c229e28df5a01d013bf23aa93ada9ca674c3ec01a3b993659113e5399a3f8f84c43119dfc419d71db26952f4e16cfce1cbfff184686
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
- # If the block we yield to here raises an error that is not caught below the `true` will not get hit.
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 re_try
181
- transaction_error_logger(object: object, error: error, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context} 2nd attempt]")
182
- Activerecord::Transactionable::Result.new(false, context: local_context, transaction_open: transaction_open, error: error, type: 'retriable') # <= make the return value meaningful. Meaning is: transaction failed after two attempts
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} 1st attempt]")
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
- transaction_error_logger(object: nil, error: error, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
193
- Activerecord::Transactionable::Result.new(false, context: local_context, transaction_open: transaction_open, error: error, type: 'already_added') # <= make the return value meaningful. Meaning is: transaction failed
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
- transaction_error_logger(object: object, error: error, additional_message: " [#{transaction_open ? 'nested ' : ''}#{local_context}]")
196
- Activerecord::Transactionable::Result.new(false, context: local_context, transaction_open: transaction_open, error: error, type: 'needing_added') # <= make the return value meaningful. Meaning is: transaction failed
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
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Transactionable
3
- VERSION = '2.0.3'.freeze
3
+ VERSION = '2.0.4'.freeze
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: 2.0.3
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-12 00:00:00.000000000 Z
11
+ date: 2018-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel