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

Don't clear transaction state after manual rollback #32862

Merged
merged 1 commit into from May 13, 2018

Conversation

eugeneius
Copy link
Member

See #32796 (comment).

If an ActiveRecord::Rollback error was raised by a persistence method (e.g. in an after_save callback), this logic would potentially discard the original state of the record from before the transaction, preventing it from being restored later when the transaction was rolled back.


This code was originally added in #5535, ironically to address the same problem being fixed here!

The transaction wrapped around a save call is itself wrapped with rollback_active_record_state!, which restores the record's original state if save raises an error. Raising ActiveRecord::Rollback rolls back the transaction without raising an error, so rollback_active_record_state! would incorrectly discard the record's original state instead of restoring it, as if the transaction had been committed.

#5535 solved this by decrementing the level counter that ensures only the outermost nested block restores the original state, letting the transaction restore it instead of rollback_active_record_state!.

(Incidentally, a better fix would have been to delete rollback_active_record_state!; it's vestigial.)

The tests added with the original fix used an observer instead of a callback to demonstrate the problem, and were removed in ccecab3 when observers were extracted to a separate gem.

Later when #9068 was merged, the behaviour regressed, because the record's original state is now lazily restored the first time it's accessed after the transaction, instead of immediately when a rollback occurs. So instead of the state being restored when the transaction rolled back, it was discarded by rollback_active_record_state! before the application had a chance to trigger the restore.

If an `ActiveRecord::Rollback` error was raised by a persistence method
(e.g. in an `after_save` callback), this logic would potentially discard
the original state of the record from before the transaction, preventing
it from being restored later when the transaction was rolled back.
@rails-bot
Copy link

r? @schneems

(@rails-bot has picked a reviewer for you, use r? to override)

@Edouard-chin
Copy link
Member

Thanks! 💯 🥇

The transaction wrapped around a save call is itself wrapped with rollback_active_record_state!,

I'm not familiar with that part of the code but I noticed that save is indeed already wrapped inside rollback_active_record_state but it's not the case for save!
Do you know why? Should we also have a test using create! to ensure the behavior works the same way as create ?

On a side note I think this PR will also fix #32848

@eugeneius
Copy link
Member Author

rollback_active_record_state! was removed from save! but not save in da840d1. I believe that leaving it in save was a mistake, since that commit was intended to move the rollback logic from the save/save! call to the transaction stack.

The lighthouse ticket for the patch describes why rollback_active_record_state! isn't sufficient:
https://rails.lighthouseapp.com/projects/8994/tickets/2991

I'd like to remove rollback_active_record_state! entirely; I'm pretty certain that the only effect it's had in the last 9 years is to occasionally cause bugs. I considered doing it in this pull request, but the history here is already hard enough to follow. I'll open another pull request to remove it - then the logic for save and save! when they raise an exception will be equivalent, and we won't need two tests.

I checked the repro scripts from #32847 and #32848 on this branch, and they both still fail. I think I have an idea of what's causing both issues though; I'll triage them later.

@kamipo kamipo merged commit 8cfa346 into rails:master May 13, 2018
gmalette added a commit to gmalette/rails that referenced this pull request May 14, 2018
Once rails#32862 is merged, rolling back a record will rollback it's state to match
the state before the database changes were applied
kamipo added a commit that referenced this pull request May 15, 2018
Don't clear transaction state after manual rollback
rafaelfranca pushed a commit that referenced this pull request May 22, 2018
* Rollback parent transaction when children fails to update

Rails supports autosave associations on the owner of a `has_many`
relationship. In certain situation, if the children of the association
fail to save, the parent is not rolled back.

```ruby
class Employee < ActiveRecord::Base
end

class Company < ActiveRecord::Base
  has_many(:employees)
end

company = Company.new
employee = company.employees.new
company.save
```

In the previous example, if the Employee failed to save, the Company
will not be rolled back. It will remain in the database with no
associated Employee.

I expect the `company.save` call to be atomic, and either create all or
none of the records.

The persistance of the Company already starts a transaction that nests
it's children. However, it didn't track the success or failure of it's
children in this very situation, and the outermost transaction is not
rolled back.

This PR makes the change to track the success of the child insertion and
rollback the parent if any of the children fail.

* Change the test to reflect what we expect

Once #32862 is merged, rolling back a record will rollback it's state to match
the state before the database changes were applied

* Use only the public API to express the tests

* Refactor to avoid reassigning saved for nested reflections

[Guillaume Malette + Rafael Mendonça França]
rafaelfranca pushed a commit that referenced this pull request May 22, 2018
* Rollback parent transaction when children fails to update

Rails supports autosave associations on the owner of a `has_many`
relationship. In certain situation, if the children of the association
fail to save, the parent is not rolled back.

```ruby
class Employee < ActiveRecord::Base
end

class Company < ActiveRecord::Base
  has_many(:employees)
end

company = Company.new
employee = company.employees.new
company.save
```

In the previous example, if the Employee failed to save, the Company
will not be rolled back. It will remain in the database with no
associated Employee.

I expect the `company.save` call to be atomic, and either create all or
none of the records.

The persistance of the Company already starts a transaction that nests
it's children. However, it didn't track the success or failure of it's
children in this very situation, and the outermost transaction is not
rolled back.

This PR makes the change to track the success of the child insertion and
rollback the parent if any of the children fail.

* Change the test to reflect what we expect

Once #32862 is merged, rolling back a record will rollback it's state to match
the state before the database changes were applied

* Use only the public API to express the tests

* Refactor to avoid reassigning saved for nested reflections

[Guillaume Malette + Rafael Mendonça França]
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.

None yet

5 participants