-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for asynchronous creation/removal of indexes
- Loading branch information
Showing
40 changed files
with
1,454 additions
and
133 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
# Background Schema Migrations | ||
|
||
When a project grows, your database starts to be heavy and performing schema changes through the deployment process can be very painful. | ||
|
||
E.g., for very large tables, index creation can be a challenge to manage. While adding indexes `CONCURRENTLY` creates indexes in a way that does not block ordinary traffic, it can still be problematic when index creation runs for many hours. Necessary database operations like autovacuum cannot run, and the deployment process is usually blocked waiting for index creation to finish. | ||
|
||
**Note**: You probably don't need to use this feature for smaller projects, since performing schema changes directly on smaller databases will be perfectly fine and will not block the deployment too much. | ||
|
||
## Installation | ||
|
||
Make sure you have migration files generated when installed this gem: | ||
|
||
```sh | ||
$ bin/rails generate online_migrations:install | ||
``` | ||
|
||
Start a background migrations scheduler. For example, to run it on cron using [whenever gem](https://github.com/javan/whenever) add the following lines to its `schedule.rb` file: | ||
|
||
```ruby | ||
every 1.minute do | ||
runner "OnlineMigrations.run_background_schema_migrations" | ||
end | ||
``` | ||
|
||
or run it manually when the deployment is finished, from the rails console: | ||
|
||
```rb | ||
[production] (main)> OnlineMigrations.run_background_schema_migrations | ||
``` | ||
|
||
**Note**: Scheduler will perform only one migration at a time, to not load the database too much. If you enqueued multiple migrations or a migration for multiple shards, you need to call this method a few times. | ||
|
||
**Note**: Make sure that the process that runs the scheduler does not die until the migration is finished. | ||
|
||
## Enqueueing a Background Schema Migration | ||
|
||
Currently, only helpers for adding/removing indexes are provided. | ||
|
||
Background schema migrations should be performed in 2 steps: | ||
|
||
1. Create a PR that schedules the index to be created/removed | ||
2. Verify that the PR was deployed and that the index was actually created/removed on production. | ||
Create a follow-up PR with a regular migration that creates/removes an index synchronously (will be a no op when run on production) and commit the schema changes for `schema.rb`/`structure.sql` | ||
|
||
To schedule an index creation: | ||
|
||
```ruby | ||
# db/migrate/xxxxxxxxxxxxxx_add_index_to_users_email_in_background.rb | ||
def up | ||
add_index_in_background(:users, :email, unique: true) | ||
end | ||
``` | ||
|
||
To schedule an index removal: | ||
|
||
```ruby | ||
# db/migrate/xxxxxxxxxxxxxx_remove_index_from_users_email_in_background.rb | ||
def up | ||
remove_index_in_background(:users, name: "index_users_on_email") | ||
end | ||
``` | ||
|
||
`add_index_in_background`/`remove_index_in_background` accept additional configuration options which controls how the background schema migration is run. Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/background_schema_migrations/migration_helpers.rb) for the list of all available configuration options. | ||
|
||
## Instrumentation | ||
|
||
Background schema migrations use the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API. | ||
|
||
You can subscribe to `background_schema_migrations` events and log it, graph it, etc. | ||
|
||
To get notified about specific type of events, subscribe to the event name followed by the `background_schema_migrations` namespace. E.g. for retries use: | ||
|
||
```ruby | ||
# config/initializers/online_migrations.rb | ||
ActiveSupport::Notifications.subscribe("retried.background_schema_migrations") do |name, start, finish, id, payload| | ||
# background schema migration object is available in payload[:background_schema_migration] | ||
|
||
# Your code here | ||
end | ||
``` | ||
|
||
If you want to subscribe to every `background_schema_migrations` event, use: | ||
|
||
```ruby | ||
# config/initializers/online_migrations.rb | ||
ActiveSupport::Notifications.subscribe(/background_schema_migrations/) do |name, start, finish, id, payload| | ||
# background schema migration object is available in payload[:background_schema_migration] | ||
|
||
# Your code here | ||
end | ||
``` | ||
|
||
Available events: | ||
|
||
* `started.background_schema_migrations` | ||
* `run.background_schema_migrations` | ||
* `completed.background_schema_migrations` | ||
* `retried.background_schema_migrations` | ||
* `throttled.background_schema_migrations` | ||
|
||
## Monitoring Background Schema Migrations | ||
|
||
Background Schema Migrations can be in various states during its execution: | ||
|
||
* **enqueued**: A migration has been enqueued by the user. | ||
* **running**: A migration is being performed by a migration executor. | ||
* **failing**: A migration raised an exception during last run (or last retry) and will be retried. | ||
* **failed**: A migration raises an exception when running and won't be retried anymore. | ||
* **succeeded**: A migration finished without error. | ||
|
||
## Configuring | ||
|
||
There are a few configurable options for the Background Schema Migrations. Custom configurations should be placed in a `online_migrations.rb` initializer. | ||
|
||
Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/background_schema_migrations/config.rb) for the list of all available configuration options. | ||
|
||
**Note**: You can dynamically change certain migration parameters while the migration is run. | ||
For example, | ||
```ruby | ||
migration = OnlineMigrations::BackgroundSchemaMigrations::Migration.find(id) | ||
migration.update!( | ||
statement_timeout: 2.hours, # The statement timeout value used when running the migration | ||
max_attempts: 10 # The # of attempts the failing migration will be retried | ||
) | ||
``` | ||
|
||
### Customizing the error handler | ||
|
||
Exceptions raised while a Background Schema Migration is performing are rescued and information about the error is persisted in the database. | ||
|
||
If you want to integrate with an exception monitoring service (e.g. Bugsnag), you can define an error handler: | ||
|
||
```ruby | ||
# config/initializers/online_migrations.rb | ||
|
||
OnlineMigrations.config.background_schema_migrations.error_handler = ->(error, errored_migration) do | ||
Bugsnag.notify(error) do |notification| | ||
notification.add_metadata(:background_schema_migration, { name: errored_migration.name }) | ||
end | ||
end | ||
``` | ||
|
||
The error handler should be a lambda that accepts 2 arguments: | ||
|
||
* `error`: The exception that was raised. | ||
* `errored_migration`: An `OnlineMigrations::BackgroundSchemaMigrations::Migration` object that represents a failed migration. | ||
|
||
### Multiple databases and sharding | ||
|
||
If you have multiple databases or sharding, you may need to configure where background migrations related tables live | ||
by configuring the parent model: | ||
|
||
```ruby | ||
# config/initializers/online_migrations.rb | ||
|
||
# Referring to one of the databases | ||
OnlineMigrations::ApplicationRecord.connects_to database: { writing: :animals } | ||
|
||
# Referring to one of the shards (via `:database` option) | ||
OnlineMigrations::ApplicationRecord.connects_to database: { writing: :shard_one } | ||
``` | ||
|
||
By default, ActiveRecord uses the database config named `:primary` (if exists) under the environment section from the `database.yml`. | ||
Otherwise, the first config under the environment section is used. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
class CreateBackgroundSchemaMigrations < <%= migration_parent %> | ||
def change | ||
# You can remove this migration for now and regenerate it later if you do not have plans | ||
# to use background schema migrations, like adding indexes in the background. | ||
create_table :background_schema_migrations do |t| | ||
t.bigint :parent_id | ||
t.string :migration_name, null: false | ||
t.string :table_name, null: false | ||
t.string :definition, null: false | ||
t.string :status, default: "enqueued", null: false | ||
t.string :shard | ||
t.boolean :composite, default: false, null: false | ||
t.integer :statement_timeout | ||
t.datetime :started_at | ||
t.datetime :finished_at | ||
t.integer :max_attempts, null: false | ||
t.integer :attempts, default: 0, null: false | ||
t.string :error_class | ||
t.string :error_message | ||
t.string :backtrace, array: true | ||
t.string :connection_class_name | ||
t.timestamps | ||
|
||
t.foreign_key :background_schema_migrations, column: :parent_id, on_delete: :cascade | ||
|
||
t.index [:migration_name, :shard], unique: true, name: :index_background_schema_migrations_on_unique_configuration | ||
end | ||
end | ||
end |
Oops, something went wrong.