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

Accept optional transaction args to #with_lock #43224

Merged

Conversation

jmileham
Copy link
Contributor

@jmileham jmileham commented Sep 15, 2021

Summary

Make #with_lock as expressive as calling #transaction and #lock!
individually to enable behavior like so:

person.with_lock("FOR UPDATE NOWAIT", requires_new: true) do
  ...
end

Helps teams who prefer #with_lock over #lock! to ensure the lock is
taken out within a transaction, even when advanced transaction control
is required without requiring redundant transaction blocks.

Other Information

This is a nice-to-have in a pretty stable part of ActiveRecord, but it would be helpful for teams who use linters to ensure there's a wrapping transaction before attempting to take out a lock, while still giving them the flexibility to, e.g. require a new nested transaction without double nesting.

Could buy the counterargument that the performance cost of the following is minimal given the default behavior of nested transactions

Person.transaction(requires_new: true) do
  person.with_lock do # no additional transaction is taken out because one's already open
    ...
  end
end

But I wanted to run it up the flagpole anyway since it feels like a coherent addition to the #with_lock method contract, is fully backward compatible, and provides a succinct way to express transaction configuration while advantaging the "safer" block syntax.

Retrospective h/t to @fractaledmind's PR #43077 which I just found as I was about to submit this PR (great minds think alike!). Stephen's PR is patching the exact same issue in maybe a more expressive way. The approach I'm submitting avoids coupling to the options that #transaction takes and passes them through blind, but it does so via some old school options munging as opposed to named arguments; I'm honestly not sure which I prefer, but if his approach was preferred, the testing approach I took would probably work well in his PR.

@jmileham jmileham force-pushed the with_lock_accepts_transaction_opts branch 2 times, most recently from 7d7eb04 to 00f8d34 Compare September 15, 2021 22:18
@@ -738,6 +738,29 @@ def test_with_lock_rolls_back_transaction
assert_equal old, person.reload.first_name
end

def test_with_lock_forwards_transaction_options
person = Person.find 1
assert_called_with(person, :transaction, [requires_new: true, isolation: "serializable", joinable: 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.

This is testing the implementation details. We avoid to do that in Rails. Can you change the test to really test if the transaction was open with the right options?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. I updated this test to introspect the resulting transaction.

Person.transaction do
outer_transaction = Person.connection.transaction_manager.current_transaction
assert_equal true, outer_transaction.joinable?
person.with_lock(requires_new: true, joinable: false) do
Copy link
Contributor Author

@jmileham jmileham Sep 16, 2021

Choose a reason for hiding this comment

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

The updated test no longer tests isolation:. Testing isolation is database-specific because they each support different isolation levels (and sqlite3 doesn't even like to have isolation specified at all since it only supports one isolation level). We forward all opts to transaction anyway, so if any work, all should work. I experimented with a test that assert_raises ActiveRecord::TransactionIsolationError when provided a nonexistent isolation type, which would work consistently across DB engines, but it felt weird to test that behavior from here. I am now testing isolation in the adapter-specific tests below since I needed to limit testing to an engine that supports transactions anyway.

end
end

if current_adapter?(:PostgreSQLAdapter)
Copy link
Contributor Author

@jmileham jmileham Sep 16, 2021

Choose a reason for hiding this comment

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

This is a pattern I saw in other tests which feels appropriate here. In order to test transaction behavior I need to do these tests on an engine that supports transactions, but it doesn't feel important to test all database engines' specific behavior at this level, so I stuck with postgresql.

@jmileham
Copy link
Contributor Author

Thanks, @rafaelfranca! This should be ready for another look. I've got meaningful end to end tests of all the transaction and locking behavior now. Happy to squash commits and rebase the changelog, but want to make sure I'm on the right track here.

@jmileham jmileham force-pushed the with_lock_accepts_transaction_opts branch 6 times, most recently from a66dd45 to d66fc65 Compare September 21, 2021 18:27
@jmileham jmileham force-pushed the with_lock_accepts_transaction_opts branch from d66fc65 to 3782cb0 Compare September 22, 2021 21:02
Make #with_lock as expressive as calling #transaction and #lock!
individually to enable behavior like so:

    person.with_lock("FOR UPDATE NOWAIT", requires_new: true) do
      ...
    end

Helps teams who prefer #with_lock over #lock! to ensure the lock is
taken out within a transaction, even when advanced transaction control
is required without requiring redundant transaction blocks.
@jmileham jmileham force-pushed the with_lock_accepts_transaction_opts branch from 3782cb0 to b5e670a Compare September 23, 2021 13:43
@rafaelfranca rafaelfranca merged commit 78e7a7f into rails:main Sep 23, 2021
@rafaelfranca
Copy link
Member

Awesome! Thank you for working on this

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

Successfully merging this pull request may close these issues.

2 participants