-
Notifications
You must be signed in to change notification settings - Fork 24
Implement safe locking in unsafe_* methods when appropriate #113
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
Conversation
e1b5dc6 to
ec7ea92
Compare
67f5753 to
a044613
Compare
6561609 to
4709908
Compare
| # constraint name, we are converting name to string explicitly to solve that. | ||
| def raw_remove_check_constraint(table, name:, **options) | ||
| super(table, name: name.to_s, **options) | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I was looking to organize methods and their tests, I discovered this unicorn. Rails 7.0 is EOL, so my solution was to just remove this method and Rails 7.0 from our gemspec / build matrix
4709908 to
9bbdc6d
Compare
9bbdc6d to
6efefe0
Compare
jcoleman
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to have unsafe_statements_spec.rb now?
I left a few comments on the code, but the spec file diff is unreadable; I think the issue is that the removal of something deprecated/defaults is getting confused with the move of the unsafe methods testing. But that no tonly means that the things it thinks are changes aren’t necessarily changes, it means all of the moved stuff is just shown as all net new…Maybe the answer is a commit that moves the tests around in the file (like a pure refactor) and then a separate commit with actual changes?
| # Direct dispatch to underlying Rails method as unsafe_<method_name> with dependent object check / no locking | ||
| # | ||
| # We don't do locking on execute because it's so generic. | ||
| # For drop_table and rename_table, it's implied that the table is unused. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense for drop_table, although I don't really see what blocks having safe locking on it -- it seems like it would work as expected out of the box just like the other methods, so why not have it?
Re: rename_table: that does not imply the table is unused, necessarily, given it's often used to compose swapping live tables, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So both drop_table and rename_table are generally used together to swap live tables. I actually think if we added locking to these, a recent BT migration would not have worked...
The general operation is something like:
- acquire lock on old table (and implicitly open a transaction)
- create new table
- do some manipulation of the data to move stuff from old to new
- drop old
- rename new to old
- release lock (and commit transaction)
If we added locking to these methods, I think (5) would fail with a "nested lock detected" error.
However, if you're writing a migration that complex you might just use the raw_* methods to get around these limitations. So I guess I'm indifferent about adding locking to these methods. If something is actually still using a table when you go to drop / rename, the end result is still the same (it would just delay the catastrophe if it takes a bit of time to acquire the lock).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we added locking to these methods, I think (5) would fail with a "nested lock detected" error.
It'd fail in development though. Since we support locking multiple tables with safely_acquire_lock_for_table I think it'd be easy to fix, right?
Basically my thought comes down to this: we want to be safe by default, and safely acquiring a lock on something that's safe to drop is safe, but failing to acquire that lock safely opens us to the possibility that a mistaken table drop also locks the system up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be easy to fix, right?
Sure but then you would have to move the table creation outside the lock. Can't lock a table that doesn't exist!
But yeah, I see your point. I don't feel too strongly about all this so I'll go ahead and make this change.
A somewhat related question... would it add safety to have some kind of timeout / retry limit for lock acquisition? As it stands safely_acquire_lock_for_table just keeps trying indefinitely, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A somewhat related question... would it add safety to have some kind of timeout / retry limit for lock acquisition? As it stands safely_acquire_lock_for_table just keeps trying indefinitely, correct?
Yeah, I think so. That sounds like a good candidate for an issue to discuss further.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
I think the diff will be actually readable if rebased on #123 |
9adbc2b to
a490a8b
Compare
7e3d6aa to
234de8c
Compare
234de8c to
fc6d5c5
Compare
| # Direct dispatch to underlying Rails method as unsafe_<method_name> with dependent object check / safe lock acquisition | ||
| delegate_unsafe_method_to_migration_base_class_with_lock :add_check_constraint | ||
| delegate_unsafe_method_to_migration_base_class_with_lock :add_column | ||
| delegate_unsafe_method_to_migration_base_class_with_lock :change_column |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we should treat this like we do change_table?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess my thinking was that anything you're trying to do with change_table can be accomplished with a combination of other, more specific, unsafe methods. Do we have that same situation with change_column? Like if you wanted to change a column type would you be forced to use raw_change_column?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like if you wanted to change a column type would you be forced to use
raw_change_column?
Hmm, that's a good point. We probably should have a way to do those things discretely (and that would allow a safe version for types, also, since some are binary compatible).
But the non-existence of those now suggests we can punt that to an issue to track it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was thinking about that a bit too. I can open an issue and we can discuss further, but I'm assuming the list of binary compatible types needs to be whittled down to type changes that Rails can transparently handle. For example, json -> text appears to be binary compatible, but I imagine Rails might not like that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
016f0c6 to
4c6a23c
Compare
Closes #63
Leaving the below context, but just a note that most of that refactoring was actually done in: #123
Okay, so this is a massive one, but the diff is so large because I used this as an opportunity to reorganize the tests and moved all of the
unsafe_*methods intounsafe_statements.rb. It became incredibly difficult to locate and modify tests to add locking expectations (and surprise some of those methods didn't even have tests), so I attempted to organize the tests into four categories:delegate_raw_method_to_migration_base_classdelegate_unsafe_method_to_migration_base_class_with_lockordelegate_unsafe_method_to_migration_base_classThis additionally removes support for Rails 7.0 (in a separate commit) which gets rid of a fifth type of method: