-
Notifications
You must be signed in to change notification settings - Fork 897
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
Add a patch to ActiveRecord::Migration for tracking replicated migrations #17919
Add a patch to ActiveRecord::Migration for tracking replicated migrations #17919
Conversation
provider_region = MiqRegion.find(s.provider_region) | ||
|
||
while !provider_region.migrations_ran.include?(version) do | ||
s.disable |
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'm trying understand the purpose of 3 lines. Why do we need to disable, enable the subscription and then sleep here?
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 is to ensure that the apply process "wakes up" at every migration to consume "queued" changes from the remote region. Really we only need to do this if the subscription is "down" so I can make this conditional.
1a35421
to
e2fe201
Compare
lib/extensions/ar_migration.rb
Outdated
@@ -0,0 +1,46 @@ | |||
module MigrationSyncHelper | |||
def self.migrations_column_present? | |||
ActiveRecord::Base.connection.columns("miq_regions").map(&:name).include?("migrations_ran") |
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.
Would detect
be better? ActiveRecord::Base.connection.columns("miq_regions").detect { |column| column.name == "migrations_ran" }
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.
Also, should we cache this? Once the column is found, we shouldn't have to look for it anymore.
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 don't really have a preference so I can change it to the detect version as it looks slightly more performant.
I don't think we should cache because I think it would be shared across multiple calls to migrate
which would defeat the purpose.
MigrationSyncHelper.wait_for_remote_region_migration(MiqRegion.find(s.provider_region), version) | ||
end | ||
|
||
ret = super |
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.
super.tap
?
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.
Eh, kind of would rather this version because we wouldn't be using the return value of super
in the tap
block.
I think the way I have it is more explicit about returning the previous value where using tap
feels more like taking advantage of a side-effect.
e8b5061
to
2083f6a
Compare
Think this one is ready for review as well. The specs will go green once ManageIQ/manageiq-schema#266 is merged. I'm in the process of testing this out on live systems. |
lib/extensions/ar_migration.rb
Outdated
return unless (region = MiqRegion.my_region) | ||
|
||
new_migrations = ActiveRecord::SchemaMigration.normalized_versions | ||
new_migrations << version if direction == :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 don't know of any customer that's gone back down in production, but should we handle that case anyway?
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 as it turns out, ActiveRecord::SchemaMigration.normalized_versions
doesn't contain the current version, in both the down and up case. So we need to add it to the list in the up case, but don't need to remove it in the down case.
It's weird.
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.
That is strange. Since we are overwriting the column every time, it shouldn't be an issue.
So while testing this on a live system I discovered that enabling and disabling the subscription doesn't actually work from within the migration context because it takes a lock on the We effectively deadlock waiting for a change to replicate. One option is to just wait without bouncing the subscription, but I'd like to find something that won't impact migration time so much. Will investigate more tomorrow. |
549819d
to
df231f6
Compare
Resolved the deadlock by using a separate dummy model connection to do the disable and enable calls for the subscription. Was able to see this work in a for-real upgrade so this should be good for final review. |
So I originally had this, but encountered issues with load order. I was getting things like Don't want reviewers to think this wasn't a conscious decision. |
lib/extensions/ar_migration.rb
Outdated
@@ -0,0 +1,53 @@ | |||
module MigrationSyncHelper | |||
def self.migrations_column_present? | |||
ActiveRecord::Base.connection.columns("miq_regions").detect { |c| c.name == "migrations_ran" } |
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.
Minor, but prefer .any?
here over .detect
lib/extensions/ar_migration.rb
Outdated
end | ||
|
||
class HelperARClass < ActiveRecord::Base; end | ||
def self.restart_subscription(s) |
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.
Indent?
EDIT: Oh, the class before it make it look like this method is part of that class... Can you put a line break?
|
||
ret = super | ||
MigrationSyncHelper.update_local_migrations_ran(version.to_s, direction) | ||
ret |
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.
super.tap do
MigrationSyncHelper.update_local_migrations_ran(version.to_s, direction)
end
might read a little better than having a temp var.
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.
@bdunne actually suggested the same thing and I prefer the temp variable because I see the "return the original object" part of tap
s behavior as more of a side-effect rather than the primary reason for using it. If my goal is to just return the same value rather than do something with the value returned by super
I like this way better.
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 agree with not using tap since we're not actually using the proposed block variable.
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.
Although, what does super
return? ret
could be named better. I honestly have no idea what it is.
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 don't actually know either. I didn't want to use it for anything, but figured it was good to preserve 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.
Oh funny, I see .tap's return of the original value as the main feature...in that sense I read it as more of a "oh and by the way", and it happens to just have a reference to the tapped thing, but it's not required to use 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.
Same here, "I have a thing that I need to return, but before I return it, I need to do this other thing"
I agree with not using tap since we're not actually using the proposed block variable.
@jrafanie we use block syntax all the time without referencing anything that is yielded
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 can see that use case. I prefer the original for clear separation of the super from the post-super work. It's preference.
lib/extensions/ar_migration.rb
Outdated
@@ -0,0 +1,53 @@ | |||
module MigrationSyncHelper |
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.
Should this module live inside the ArPglogicalMigration namespace, so it's clear it's not for external use?
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 could. My intention here was to avoid mixing in more than just the #migrate
method to ActiveRecord::Migration
, but if we nest this module in ArPglogicalMigration
then it seems okay. I'll probably rename it to ArPglogicalMigrationHelper
though.
All of my requests are code structure, but the code itself looks good to me 👍. Before merging, how does this affect documentation? |
lib/extensions/ar_migration.rb
Outdated
return unless MigrationSyncHelper.migrations_column_present? | ||
return unless (region = MiqRegion.my_region) | ||
|
||
new_migrations = ActiveRecord::SchemaMigration.normalized_versions |
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.
Is this sorted, and if not, does it matter?
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 doesn't look like it's sorted explicitly [ref], but it doesn't matter to this implementation as we're just checking inclusion.
lib/extensions/ar_migration.rb
Outdated
new_migrations << version if direction == :up | ||
migrations_value = ActiveRecord::Base.connection.quote(PG::TextEncoder::Array.new.encode(new_migrations)) | ||
|
||
ActiveRecord::Base.connection.exec_query(<<~SQL) |
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.
Minor, but is it better to use .update
instead of .exec_query
?
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.
No? It doesn't look that way based on the implementation [ref], but I don't really have a preference.
This simplifies the logic around logging quite a bit
d8d2913
to
752dd51
Compare
lib/extensions/ar_migration.rb
Outdated
|
||
def self.update_local_migrations_ran(version, direction) | ||
return unless migrations_column_present? | ||
return unless (region = MiqRegion.my_region) |
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.
Still don't like using any models in this code.
lib/extensions/ar_migration.rb
Outdated
attr_reader :region, :subscription, :version | ||
|
||
def initialize(subscription, version) | ||
@region = MiqRegion.find_by(:region => subscription.provider_region) |
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.
Here too with the MiqRegion model.
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 I was able to put together something that I think would work here, but ran into tons of issues trying to write the spec. I just went with a model stub in the class, but if this is something you feel strongly about I can probably get something working with another day's worth of work or so.
I thought I commented about the MiqRegion thing but I don't see my comment anywhere. In any case, I don't like the use of the MiqRegion model since this is migration code, where we usually state "no models". I'd be fine with a stub model, but I think everything in the PR can be done with plain SQL..even the array stuff. |
313d4ad
to
fbbb2be
Compare
This de-couples us from the model definition during migrations
fbbb2be
to
9b48682
Compare
SELECT EXISTS( | ||
SELECT id FROM miq_regions WHERE region = #{ActiveRecord::Base.connection.quote(my_region_number)} | ||
) | ||
SQL |
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.
slick use of squiggly heredoc with raw SQL 👍
end | ||
|
||
def self.my_region_number | ||
@my_region_number ||= ApplicationRecord.my_region_number |
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 overheard you describing how you can use ApplicationRecord here but must must use AR::Base elsewhere because reasons... the flip flopping of their usage here seems to need a "why" 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.
So the only places I need to use ActiveRecord::Base
are when the code is evaluated at load-time rather than runtime. So technically I could use ApplicationRecord.connection
in the definitions of .migrations_column_present?
and .my_region_created?
.
Here I need to use ApplicationRecord
because we include the region-aware modules into that class rather than ActiveRecord::Base
.
The place that would cause a problem with constant loading would really be HelperARClass. But I also think I could probably get away with using an anonymous class there if I tried really hard. What do you think?
expect(t.alive?).to be true | ||
subject.region.update_attributes!(:migrations_ran => ActiveRecord::SchemaMigration.normalized_versions << version) | ||
|
||
# Wait a max of 5 seconds so we don't disrupt the whole test suite if something terrible happens |
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.
🙇
t = Thread.new do | ||
Thread.current.abort_on_exception = true | ||
subject.wait_for_remote_region_migration(0) | ||
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.
This test scares me a little. It looks like it should be fine but threading is hard.
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'm fine with merging it as is but it does make me 😨
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.
LGTM, minor nits about comments about AR::Base vs. AppliationRecord... but looks great.
In this module we lean towards ActiveRecord::Base to avoid any conflict with application logic. We also cannot use ApplicationRecord at load time because it causes a load error.
Some comments on commits carbonin/manageiq@107cd22~...30a2e2c lib/extensions/ar_migration.rb
spec/lib/extensions/ar_migration_spec.rb
|
Checked commits carbonin/manageiq@107cd22~...30a2e2c with ruby 2.3.3, rubocop 0.52.1, haml-lint 0.20.0, and yamllint 1.10.0 lib/extensions/ar_migration.rb
|
Add a patch to ActiveRecord::Migration for tracking replicated migrations (cherry picked from commit cc2043f) Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1601015
Hammer backport details:
|
This will fix https://bugzilla.redhat.com/show_bug.cgi?id=1601015 by only allowing the global region to execute a particular migration if it is sure that migration was successfully executed and replicated from all of the remote regions.
This ensures that we will never "miss" a schema state and get ourselves into a situation where we cannot continue replicating changes from a remote region.
Requires: ManageIQ/manageiq-schema#266
Alternative to: ManageIQ/manageiq-schema#264