Skip to content

Commit

Permalink
Do not run multiple background schema migrations on the same table at…
Browse files Browse the repository at this point in the history
… the same time
  • Loading branch information
fatkodima committed Apr 30, 2024
1 parent 71433d8 commit 3f1c222
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## master (unreleased)

- Do not run multiple background schema migrations on the same table at the same time

## 0.17.1 (2024-04-28)

- Fix raising in development when using sharding and background index creation/removal was not enqueued
Expand Down
18 changes: 16 additions & 2 deletions lib/online_migrations/background_schema_migrations/scheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
module OnlineMigrations
module BackgroundSchemaMigrations
# Class responsible for scheduling background schema migrations.
# It selects a single migration and runs it if there is no currently running migration.
# It selects a single migration and runs it if there is no currently
# running migration on the same table.
#
# Scheduler should be configured to run periodically, for example, via cron.
# @example Run via whenever
Expand All @@ -19,12 +20,25 @@ def self.run

# Runs Scheduler
def run
migration = Migration.runnable.enqueued.queue_order.first || Migration.retriable.queue_order.first
migration = find_migration
if migration
runner = MigrationRunner.new(migration)
runner.run
end
end

private
def find_migration
active_migrations = Migration.running.select(:table_name, :shard).to_a
runnable_migrations = Migration.runnable.enqueued.queue_order.to_a + Migration.retriable.queue_order.to_a

runnable_migrations.find do |runnable_migration|
active_migrations.none? do |active_migration|
active_migration.shard == runnable_migration.shard &&
active_migration.table_name == runnable_migration.table_name
end
end
end
end
end
end
53 changes: 44 additions & 9 deletions test/background_schema_migrations/scheduler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ def teardown
end

def test_run
m = create_migration
m = create_migration(
name: "index_dogs_on_name",
table_name: "dogs",
definition: 'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "index_dogs_on_name" ON "dogs" ("name")',
connection_class_name: "ShardRecord"
)
child1, child2, child3 = m.children.to_a

scheduler = OnlineMigrations::BackgroundSchemaMigrations::Scheduler.new
Expand All @@ -28,7 +33,12 @@ def test_run
end

def test_run_retries_failed_migrations
m = create_migration
m = create_migration(
name: "index_dogs_on_name",
table_name: "dogs",
definition: 'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "index_dogs_on_name" ON "dogs" ("name")',
connection_class_name: "ShardRecord"
)
child = m.children.first

scheduler = OnlineMigrations::BackgroundSchemaMigrations::Scheduler.new
Expand All @@ -46,14 +56,39 @@ def test_run_retries_failed_migrations
assert m.children.all?(&:succeeded?)
end

def test_run_when_on_the_same_table_already_running
connection = ActiveRecord::Base.connection
connection.create_table(:users, force: true) do |t|
t.string :email
t.string :name
end

m1 = create_migration(
name: "index_users_on_email",
table_name: "users",
definition: 'CREATE UNIQUE INDEX CONCURRENTLY "index_users_on_email" ON "users" ("email")'
)
m1.update_column(:status, :running) # emulate running migration

_m2 = create_migration(
name: "index_users_on_name",
table_name: "users",
definition: 'CREATE INDEX CONCURRENTLY "index_users_on_name" ON "users" ("name")'
)

assert_equal 1, OnlineMigrations::BackgroundSchemaMigrations::Migration.running.count

scheduler = OnlineMigrations::BackgroundSchemaMigrations::Scheduler.new
scheduler.run

assert_equal 1, OnlineMigrations::BackgroundSchemaMigrations::Migration.running.count
ensure
connection.drop_table(:users)
end

private
def create_migration
ActiveRecord::Base.connection.create_background_schema_migration(
"index_dogs_on_name",
"dogs",
definition: 'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "index_dogs_on_name" ON "dogs" ("name")',
connection_class_name: "ShardRecord"
)
def create_migration(name:, table_name:, **options)
ActiveRecord::Base.connection.create_background_schema_migration(name, table_name, **options)
end
end
end

0 comments on commit 3f1c222

Please sign in to comment.