Rails has for a long time had a feature of
silently committing a transaction
return is called from within it.
It’s done this way to not leave an open
transaction on the connection.
That same feature can also be used to return early from a transaction if some condition is not met.
def destroy_post_if_invalid Post.transaction do post = Post.find_by(id: id) return if post.valid? post.destroy end end
This behavior of committing transactions silently can bring surprises when used in combination with the Ruby’s timeout method.
In the following example, the transaction will commit even if it doesn’t finish in 1 second.
Timeout.timeout(1) do Post.transaction do # do some heavy lifting # post.time_consuming_task # simulate something slow sleep 3 end end
It works this way because
timeout uses throw
to exit the transaction block when the
argument is not provided.
# uses throw and catch to exit the block >> Timeout.timeout(1) do sleep 2 end Timeout::Error: execution expired # will raise ArgumentError >> Timeout.timeout(1, ArgumentError) do sleep 2 end ArgumentError: execution expired
There is no backward-compatible solution to the problem, as stated by the author of the PR which deprecates this surprising behavior without replacement.
Unfortunately, the ensure block can’t distinguish between a block exited with return, break or throw, so we can’t fix the problem with throw as used in Timeout.timeout
We will now start seeing the following deprecation warning in Rails 6.1.
DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is deprecated without replacement. If the `throw` came from `Timeout.timeout(duration)`, pass an exception class as a second argument so it doesn't use `throw` to abort its block. This results in the transaction being committed, but in the next release of Rails it will raise and rollback.
The solution going forward is to use
inside of transactions rather than
return and using an
exception class with the
def destroy_post_if_invalid Post.transaction do post = Post.find_by(id: id) unless post.valid? do post.destroy end end end