Deprecate committing a transaction exited with return or throw#29333
Deprecate committing a transaction exited with return or throw#29333jeremy merged 2 commits intorails:masterfrom
Conversation
|
Thanks for the pull request, and welcome! The Rails team is excited to review your changes, and you should hear from @rafaelfranca (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. This repository is being automatically checked for code quality issues using Code Climate. You can see results for this analysis in the PR status below. Newly introduced issues should be fixed before a Pull Request is considered ready to review. Please see the contribution instructions for more information. |
|
We aren't going to add a deprecation to an existing release series; this needs to be directed to master.
That doesn't seem acceptable either. The reason for the previous change is that silently rolling back on a non-error |
d60726b to
0f67779
Compare
|
Changed the base branch to master. Would it be more acceptable to raise an exception along with rolling back in the ensure block in the Rails release following the deprecation so it doesn't silently support either |
|
Yeah, while unfortunate, I think that would at least avoid nasty surprises. My preference would be to silently rollback on any throw (i.e., treating it like an exception), while silently committing if we're unwinding due to a return. That's basically the theory behind the current behaviour: no-one uses throw/catch, so a non-exception unwind is most likely a return. And that theory worked well for about 8 years. 🙈 Short of petitioning for a new language feature, the only option that comes to mind to distinguish them would be to monkey-patch I wonder how terrible that would actually be... |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
5315007 to
7620dea
Compare
|
I changed the description of the future behaviour in the exception message: - in the next release of Rails it will instead rollback.
+ in the next release of Rails it will raise and rollback.and resolved merge conflicts |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
If a transaction is wrapped in a Timeout.timeout(duration) block, then the transaction will be committed when the transaction block is exited from the timeout, since it uses `throw`. Ruby code doesn't have a way to distinguish between a block being exited from a `return`, `break` or `throw`, so fixing this problem for the case of `throw` would require a backwards incompatible change for block exited with `return` or `break`. As such, the current behaviour so it can be changed in the future.
7620dea to
c8fac62
Compare
|
We're working through this deprecation at GitHub and it feels like this is overreaching a bit. There are cases where you might want to return early and this deprecation reads like early returns will just start throwing exceptions in Rails 6.2. How are these changes intended to work with the following: with_lock do
return if some_crtieria_met?
# do work
endWill this start raising in 6.2? I've read this PR a few times and we're having trouble figuring out if our use case is intended to be unsupported or this is just a deprecation that's not applicable for us. I'm a little worried this is confusing for upgrades in other apps as well. |
|
That is an interesting use case, since in your early return example it doesn't really matter if the transaction is committed or rolled back since there hasn't been a write to the database. As a result, there is also no concern about ambiguity of whether the early return is supposed to mean the transaction should be committed or rolled back. I'm more concerned about the case where the application writes to the database and then does an early return with the intention of committing the transaction, since we can't support that without also committing the transaction in the case of a timeout from a To support your use case, perhaps we should use The other question is whether we should even bother to raise an exception in Rails 6.2 in place of the deprecation warning after rolling back the transaction. If we allow early returns out of a transaction without writes, then it would be simpler remove the write detection code and just always rollback from the transaction. The simpler behaviour would avoid the need to explain the special case to the user in documentation. It would also avoid translating a timeout into a different exception. I think the only advantage of raising an exception would be to make the breaking change more visible and thus easier to notice if the warning wasn't noticed, but the rollback from an early return seems pretty likely to be noticed from test suites if the deprecation isn't noticed. The ability to rollback the transaction through |
|
I think raising in 6.2 if there is ever a We're happy to ignore this deprecation as long as this won't start raising in 6.2. I don't know if using I'm not sure what the right path forward. This change, while fixing a real issue, can be confusing and fixes one case while introducing issues in another case. |
|
Yeah, I'm with you now about not raising in rails 6.2 If the deprecation warning is ignored because it isn't appropriate in some cases, then it won't be effective in letting developers know where to update their code to avoid being effected by the breaking change. I realize the I'll open a PR with my proposed path forward. |
|
Honestly I think this PR is causing a whole lot more trouble than it's worth. There's lots of code built up over the years that |
|
I would definitely support To avoid this problem with I think you might be overestimating how much effort it would take to refactor code to avoid the early return, especially if you compare it with refactoring code wrapped in a
Similar to what you would do with any method with deeply nested conditions, you can extract method a method to avoid a deeply nested or long method. In this case, you could extract the transaction block into a method and use What you completely skipped over is the underlying problem that can also be encountered with direct usage of Note that an |
|
@dylanahsmith instead of pulling out the inside of a transaction into it's own function just to be able to use Just PS though, the example you gave of Timeout.timeout(1) do
Example.transaction do
example.update_attributes(value: 1)
sleep 3 # simulate something slow
end
endstill isn't safe to use - what if the Timeout raises in the middle of Anyway, @dylanahsmith thank you for hearing me out, and please consider the suggestion about leaving us the |
|
You can absolutely use If you don't get a deprecation warning with your code, then it shouldn't break from the corresponding breaking change. We know very well how important this is for upgrading old code to prepare for a new release. |
|
So what now is the canonical way to return from the current context (let's assume you're nested inside several blocks so |
Is this a bug that the following code __silently__ rolls back any updates from the transaction without any exception? This is on Rails 7.0.2.4, also checked the current master: |
|
@sam0x17 @majkelcc commenting on 2 year old PRs will likely not get the attention you need. If you have found a bug, please open a new issue with a reproduction script. Some newer changes have been made to transactions that were not caused by this PR. A bug in 7.x was recently fixed in #44955 which may be related to your current issues. |
|
@eileencodes thanks, I'll create a new bug report. It looks like my issue is caused by this PR - deprecation in Rails 6 turned into a rollback in Rails 7, but the fact that it does it silently is completely unexpected. |
|
This PR was just a deprecation. The intention was to have it raise in rails 7 so that it didn't rollback silently.
Extracting the contents of the transaction into its own method would make it easier to return out of those nested blocks while still committing. In that case, the return value returned from that extracted method might be needed to conditionally skip over any code that follows the transaction in that original method. |
That was my impression after reading this topic - created a bug report here #45017. |
|
being a bit of a necromant by reviving this thread, just wanted to share the syntax I use for this: object.transaction do
post = Post.find_by_id(123)
return unless post
do_stuff(post)
endinto return unless object.transaction do
post = Post.find_by_id(123)
next unless post
do_stuff(post)
true
endthis way you can avoid extra nested |
Problem
I found that we had transactions that were being committed because of the use of
Timeout.timeout(duration) dowrapping database transactions. The reason why this was happening is that when Timeout.timeout isn't given a second argument (an exception class) then it usesthrowandcatchto exit the transaction block, after whichTimeout.timeoutwill raise an exception. This why the documentation for Timeout.timeout saysThis can be reproduced using ruby code like
where the update will be committed even if the block gets a timeout exception.
The reason why this results in the transaction being committed is that fc83920 was added to commit a transaction when the transaction block is exited with
return. Unfortunately, theensureblock can't distinguish between a block exited withreturn,breakorthrow, so we can't fix the problem withthrowas used inTimeout.timeoutwithout a backwards incompatible change.Solution
Set a local variable
completed = trueafter theyieldso that we can detect when theensureblock didn't complete, then use a deprecation warning to try to let developers know that rails will rollback instead of commit in this case in the future.That way we can fix this behaviour in the next release for the
Timeout.timeoutcase. I will open a PR against master doing this.