Skip to content

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

Closed
wants to merge 2 commits into from

Conversation

the-wendell
Copy link

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:

  • Wrote good commit messages.
  • Commit message starts with [Fix #issue-number] (if the related issue exists).
  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Added an entry to the Changelog if the new code introduces user-observable changes. See changelog entry format.
  • The PR relates to only one subject with a clear title
    and description in grammatically correct, complete sentences.
  • Run bundle exec rake default. It executes all tests and RuboCop for itself, and generates the documentation.
  • If this is a new cop, consider making a corresponding update to the Rails Style Guide.

@@ -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
Copy link
Member

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'
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.

Copy link
Author

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
Copy link
Member

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?

Copy link
Author

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)
Copy link
Member

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

@koic
Copy link
Member

koic commented Jan 5, 2021

I have never used transaction(requires_new: true) of Active Record and have not been able to read and survey https://makandracards.com/makandra/42885-nested-activerecord-transaction-pitfalls.
(I remember a little about what I experienced at J2EE a long time ago :-)

I think that the following investigations are probably needed for cop documentation and implementation.

  • If there is an option that conflicts with transaction(requires_new: true), this cop will not offense for it when the conflict option is specified
  • About the background to use with joinable: false
  • Trade-offs when using transaction(requires_new: true). I think there is a reason why transaction(requires_new: true) is not default in Rails.

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][])
Copy link
Member

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.'
Copy link
Member

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

@the-wendell
Copy link
Author

I have never used transaction(requires_new: true) of Active Record and have not been able to read and survey https://makandracards.com/makandra/42885-nested-activerecord-transaction-pitfalls.
(I remember a little about what I experienced at J2EE a long time ago :-)

I think that the following investigations are probably needed for cop documentation and implementation.

* If there is an option that conflicts with `transaction(requires_new: true)`, this cop will not offense for it when the conflict option is specified

* About the background to use with `joinable: false`

* Trade-offs when using `transaction(requires_new: true)`. I think there is a reason why `transaction(requires_new: true)` is not default in Rails.

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 sweat_smile

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 joinable: false. I think it might make sense to allow the cop to be configured for either requires_new: true or joinable: false or both.

Where should detailed documentation/explanations about the cop go? With the examples? (This is my first attempted contribution for any sort of rubocop rule)

@koic
Copy link
Member

koic commented Jan 12, 2021

Where should detailed documentation/explanations about the cop go? With the examples?

Please describe it in the class comment of cop. The following is an example:
https://github.com/rubocop-hq/rubocop-rails/blob/v2.9.1/lib/rubocop/cop/rails/unique_validation_without_index.rb#L6-L15

@stokarenko
Copy link

@koic @the-wendell

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)'))
Copy link

@stokarenko stokarenko Mar 6, 2021

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants