-
-
Notifications
You must be signed in to change notification settings - Fork 274
Fix #400 Add Rails/TransactionRequiresNew cop #414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -695,6 +694,11 @@ Rails/TimeZone: | |||
- strict | |||
- flexible | |||
|
|||
Rails/TransactionRequiresNew: | |||
Description: 'When using a transaction requires that the argument `requires_new: true` is passed.' | |||
Enabled: true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know, it's not a common rule. I think it should be Enable: false
by default.
Rails/TransactionRequiresNew: | ||
Description: 'When using a transaction requires that the argument `requires_new: true` is passed.' | ||
Enabled: true | ||
VersionAdded: '2.10' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this auto-correction will be false because the behavior will change.
SafeAutoCorrect: false
(block | ||
(send _ :transaction ...) | ||
(args) {_}) | ||
PATTERN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you replace RESTRICT_ON_SEND = %i[transaction].freeze
?
|
||
# does NOT auto-correct when arguments are present | ||
expect_correction(<<~RUBY) | ||
User.transaction(some_option: true) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
|
||
# Does NOT auto-correct when arugments are present | ||
expect_correction(<<~RUBY) | ||
ActiveRecord::Base.transaction(some_option: false) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it can be auto-corrected for this offense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I suppose it can be. I imagine that it will be safe enough to simply add requires_new: true
as the last argument in a transaction.
end | ||
|
||
# good | ||
ActiveRecord::Base.transaction(requires_new: true) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it okay not to specify joinable: false
together?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that they work independently. requires_new: true
is for custom tranaction (transaction do ... end
) and joinable: false
is for the transactions created by ActiveRecord
methods such as update!
Anyways, I will do some research and testing on this. I have read many blogs/articles about it so far but none of them seem super clear on this topic. I will try to figure it out and come up with some good documentation and examples. I might even write a medium article 😂
return unless transaction?(node) && !requires_new_argument_passed?(node) | ||
|
||
if arguments_present?(node) | ||
add_offense(node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it is possible to add_offense
whenever it will be auto-correction :-)
I have never used I think that the following investigations are probably needed for cop documentation and implementation.
There may still be others... Anyway, I think it is difficult for users to understand and use it if the information for using this cop is not documented. Story of transactions is complicated 😅 |
@@ -4,6 +4,7 @@ | |||
|
|||
### Changes | |||
|
|||
* [#410](https://github.com/rubocop-hq/rubocop-rails/pull/410): Add new`Rails/TransactionRequiresNew` cop. ([@the-wendell][]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This entry belongs to the ### New features
category. Can you create category and move to it?
@@ -695,6 +694,11 @@ Rails/TimeZone: | |||
- strict | |||
- flexible | |||
|
|||
Rails/TransactionRequiresNew: | |||
Description: 'When using a transaction requires that the argument `requires_new: true` is passed.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the reference will help users.
Reference: https://makandracards.com/makandra/42885-nested-activerecord-transaction-pitfalls
I will do some more research and come up with some examples to add to the documentation to make it more clear. As well, I will clarify the appropriate use of Where should detailed documentation/explanations about the cop go? With the examples? (This is my first attempted contribution for any sort of rubocop rule) |
Please describe it in the class comment of cop. The following is an example: |
There is an example right from a months of debugging and frustration: class UserAssinment
class << self
def create_progress!
Progress.transaction do
Progress.create!
raise 'API error'
end
end
def assign!
create_progress!
rescue => e
logger.warn e
end
end
end
UserAssinment.create_progress!
p Progress.count # 0 as expected
UserAssinment.assign!
p Progress.count # 0 as expected
UserAssinment.transaction { UserAssinment.assign! }
p Progress.count # 1 surprise! The point is that only the root transaction is a real transaction, any nested one is just an illusion by AR defaults. So exception blows the nested transaction block, but going to be rescued upper the stack and never hit the root transaction. So - root transaction will never be rolled back and will commit the part of orphaned queries above the exception. Even worse, the behavior depends on the depth of nested transaction and may (and will) be blinking across refactorings and service classes which call another service classes. |
add_offense(node) | ||
else | ||
add_offense(node) do |corrector| | ||
corrector.replace(node, node.source.gsub('.transaction', '.transaction(requires_new: true)')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transaction do
may be called as own Model method and without a dot at the beginning. Cop is catching such calls while auto-correct is running into infinitive loop
corrector.replace(node, node.source.gsub('.transaction', '.transaction(requires_new: true)')) | |
corrector.replace(node, node.source.gsub('transaction', 'transaction(requires_new: true)')) |
Anyway this gsub
is wide enough and becomes to be even wider with suggestion above, affecting the whole block including comments, nested method calls like
myvar.transactional_user_actions(foo: :bar) # before
myvar.transaction(requires_new: true)al_user_actions(foo: :bar) # after
etc
Replace this text with a summary of the changes in your PR.
The more detailed you are, the better.
Before submitting the PR make sure the following are checked:
[Fix #issue-number]
(if the related issue exists).master
(if not - rebase it).and description in grammatically correct, complete sentences.
bundle exec rake default
. It executes all tests and RuboCop for itself, and generates the documentation.