Skip to content
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

Add new Rails/PrivateTransactionOption cop #1236

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

wata727
Copy link
Contributor

@wata727 wata727 commented Feb 10, 2024

This PR adds a new cop called Rails/PrivateTransactionOption.

This cop checks whether ActiveRecord::Base.transaction(joinable: _) is used. The joinable option is a private API and is not intended to be called from outside Active Record core.

Passing joinable: false may cause unexpected behavior such as the after_commit callback not firing at the appropriate time.

See also kufu/activerecord-bitemporal#156

The motivation for wanting to add this is that even though the joinable is undocumented, it was recommended in some articles, so it may be inadvertently widely used. It is possible that it is being used incorrectly to simply enable requires_new implicitly without realizing that it affects the behavior of after_commit.


Before submitting the PR make sure the following are checked:

  • The PR relates to only one subject with a clear title and description in grammatically correct, complete sentences.
  • 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.
  • Ran bundle exec rake default. It executes all tests and runs RuboCop on its own code.
  • Added an entry (file) to the changelog folder named {change_type}_{change_description}.md if the new code introduces user-observable changes. See changelog entry format for details.
  • If this is a new cop, consider making a corresponding update to the Rails Style Guide.

This PR adds a new cop called `Rails/PrivateTransactionOption`.

This cop checks whether `ActiveRecord::Base.transaction(joinable: _)`
is used. The `joinable` option is a private API and is not intended
to be called from outside Active Record core.

rails/rails#39912 (comment)
rails/rails#46182 (comment)

Passing `joinable: false` may cause unexpected behavior such as the
`after_commit` callback not firing at the appropriate time.
@koic
Copy link
Member

koic commented Feb 10, 2024

Can you first propose an addition to the Rails Style Guide?
https://github.com/rubocop/rails-style-guide

I'm not sure which articles have made incorrect references, but including a description in the Style Guide that RuboCop Rails follows would serve as one countermeasure to them.

@wata727
Copy link
Contributor Author

wata727 commented Feb 11, 2024

Opened rubocop/rails-style-guide#355.

I'm not sure which articles have made incorrect references

A quick Google search shows the following articles:

Some articles mention unintended side effects, while others do not.

@pirj
Copy link
Member

pirj commented Feb 11, 2024

@pirj
Copy link
Member

pirj commented Feb 11, 2024

#414 Related

@wata727
Copy link
Contributor Author

wata727 commented Feb 11, 2024

documents joinable while not the recent. What happened?

See rails/rails#46186

@pirj
Copy link
Member

pirj commented Feb 11, 2024

Some more context rspec/rspec-rails#2598 (comment) - I withdraw my statement that adding joinable: false is a good practice. With requires_new: true and all explicit transactions (apart from implicit update/create/etc transactions), joinable: false is redundant. And considering the side effects (due to joinable? checks?) it’s plain bad.

@pirj
Copy link
Member

pirj commented Feb 11, 2024

Related cop suggestion #400

@wata727
Copy link
Contributor Author

wata727 commented Feb 11, 2024

Agreed with your opinion. I don't believe it's always right to require requires_new: true in all transactions, but I do believe that using joinable: false is probably wrong most of the time.

In reality, there may be situations where you have to use joinable: false because the transactions used inside a third-party gem do not use requires_new: true, but considering the side effects, it should be a last resort. This cop helps you understand that it must be used with caution and its behavior is not guaranteed by Rails core team.

@pirj
Copy link
Member

pirj commented Feb 12, 2024

Should it be possible to wrap a call to a gem that doesn’t require_new in a transaction to isolate it instead of a wrapping joinable: false?

It is worth mentioning that when reasoning about nested transactions and business logic, the error kernel design pattern comes handy.

@wata727
Copy link
Contributor Author

wata727 commented Feb 12, 2024

As far as I know, there's no way around it. You need to fix the gem.
It might be useful to add some notes on how to fix such things, but I think that is outside the scope of this cop.

Copy link
Member

@pirj pirj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few minor fixes and good to go!
Thank you

lib/rubocop/cop/rails/private_transaction_option.rb Outdated Show resolved Hide resolved
spec/rubocop/cop/rails/private_transaction_option_spec.rb Outdated Show resolved Hide resolved
spec/rubocop/cop/rails/private_transaction_option_spec.rb Outdated Show resolved Hide resolved
#
# @safety
# This Cop is unsafe because it cannot accurately identify
# the `ActiveRecord::Base.transaction` method call.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I bet transaction(joinable: true/false) would be a Rails transaction call with very high certainty.

I'd remove this clause and made the cop safe.

I would accept a single case of such a false positive from https://github.com/jeromedalbert/real-world-ruby-apps as a counter-argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is up for debate. Personally, I believe it should be marked as unsafe as long as there is theoretically a possibility of false positives.

I'd be interested to hear from other maintainers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the issue is not a false positive in the cop, but rather the change in behavior caused by the autocorrect. Therefore, shouldn't it be SafeAutoCorrect: false instead of Safe: false?

@pirj
Copy link
Member

pirj commented Jul 26, 2024

@koic @andyw8 @bbatsov WDYT?

@andyw8
Copy link
Contributor

andyw8 commented Jul 26, 2024

🤔 Should this be part of a more general cop for avoiding private ActiveRecord APIs?

@@ -782,6 +782,12 @@ Rails/Present:
# Convert usages of `unless blank?` to `if present?`
UnlessBlank: true

Rails/PrivateTransactionOption:
Description: 'Avoid use of `ActiveRecord::Base.transaction(joinable: _)`.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some reasoning could be added to the message here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed d8b64ad

@wata727 wata727 force-pushed the add_new_rails_private_transaction_option branch from 7e71ea4 to c22574f Compare July 27, 2024 08:02
@wata727
Copy link
Contributor Author

wata727 commented Jul 27, 2024

🤔 Should this be part of a more general cop for avoiding private ActiveRecord APIs?

This might be one option, but at this point, I'm not sure which other APIs should be included. Also, if we want to provide other options for each private API, it may be useful to have a Cop for each one.

@koic
Copy link
Member

koic commented Aug 16, 2024

Rails/PrivateTransactionOption cop

Can you consider naming the cop to suggest the risks associated with unexpected behavior in nested transactions, rather than focusing on the use of a private API?

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.

4 participants