online_migrations 0.14.1 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +13 -9
  4. data/docs/{background_migrations.md → background_data_migrations.md} +26 -40
  5. data/docs/background_schema_migrations.md +163 -0
  6. data/docs/configuring.md +35 -4
  7. data/lib/generators/online_migrations/background_migration_generator.rb +12 -1
  8. data/lib/generators/online_migrations/install_generator.rb +1 -1
  9. data/lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt +29 -0
  10. data/lib/generators/online_migrations/templates/initializer.rb.tt +24 -11
  11. data/lib/generators/online_migrations/templates/install_migration.rb.tt +75 -0
  12. data/lib/generators/online_migrations/templates/migration.rb.tt +7 -48
  13. data/lib/generators/online_migrations/upgrade_generator.rb +5 -0
  14. data/lib/online_migrations/application_record.rb +11 -0
  15. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -7
  16. data/lib/online_migrations/background_migrations/config.rb +27 -24
  17. data/lib/online_migrations/background_migrations/delete_associated_records.rb +1 -1
  18. data/lib/online_migrations/background_migrations/migration.rb +1 -8
  19. data/lib/online_migrations/background_migrations/migration_helpers.rb +16 -2
  20. data/lib/online_migrations/background_migrations/migration_job.rb +5 -2
  21. data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -1
  22. data/lib/online_migrations/background_migrations/migration_runner.rb +1 -1
  23. data/lib/online_migrations/background_schema_migrations/config.rb +40 -0
  24. data/lib/online_migrations/background_schema_migrations/migration.rb +205 -0
  25. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +76 -0
  26. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +110 -0
  27. data/lib/online_migrations/background_schema_migrations/migration_status_validator.rb +33 -0
  28. data/lib/online_migrations/background_schema_migrations/scheduler.rb +30 -0
  29. data/lib/online_migrations/command_checker.rb +31 -1
  30. data/lib/online_migrations/command_recorder.rb +5 -0
  31. data/lib/online_migrations/config.rb +32 -0
  32. data/lib/online_migrations/error_messages.rb +3 -3
  33. data/lib/online_migrations/lock_retrier.rb +0 -2
  34. data/lib/online_migrations/schema_statements.rb +1 -0
  35. data/lib/online_migrations/utils.rb +12 -0
  36. data/lib/online_migrations/version.rb +1 -1
  37. data/lib/online_migrations.rb +19 -2
  38. metadata +13 -4
  39. data/lib/online_migrations/background_migrations/application_record.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 330d5ed7f11aae3b85e7fb5aab5a1ea66d809da704cf193420b6d4206f72d82b
4
- data.tar.gz: 36d47a6f2a1cae3de33331a4ec9e6b21fc7b3b8b09a7d5e9c34d1be6c92e5d35
3
+ metadata.gz: 48ddbce66257c9010d8ab7361d8860cd898854207b5a1f4c6aa2b6b797aec2e1
4
+ data.tar.gz: c21fbb5f535f788e13068e339eebe77f05a378c68c00244c4522a10d50a28a0d
5
5
  SHA512:
6
- metadata.gz: e07937136867b4b6bc2d0dfb2e5ccb8471faf6ce0339a247bc0c63d591cc163e084da731e86fc01ba839c7744a51e9c63e99ce632452e471f9eed98a95b9dd02
7
- data.tar.gz: f1a33636d5fbd01f25a0cee32af0d74e377c804eaef0feee2c778c5318a93bbe8942dc03e8ea2bad0a610d0046a8d4e42b505d0991c7d215cc9842a24d5e5065
6
+ metadata.gz: a5bbff2a8cca45e5fce79d653db3246bc41a4ab9d314f44131bbf74f2659d610fdaf3e87ac5e22baf7928d0cdb481e19eacaa1ed8945e4ee32f1afac3223273f
7
+ data.tar.gz: 6e3861f420327e0bccec7ab8e7c0a508e7cf224312c366297834091467d7cdb5e4fac8a305dbb59f7c0650d199d9a17062ab0408fd180b6fbaa05946bfd27545
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.16.0 (2024-03-28)
4
+
5
+ - Add support for asynchronous creation/removal of indexes
6
+
7
+ See `docs/background_schema_migrations.md` for the feature description.
8
+
9
+ ## 0.15.0 (2024-03-19)
10
+
11
+ - Reraise errors when running background migrations inline
12
+ - Add `remove_background_migration` migration helper
13
+ - Allow adding bigint foreign keys referencing integer primary keys
14
+ - Fix `add_reference_concurrently` to check for mismatched key types
15
+
3
16
  ## 0.14.1 (2024-02-21)
4
17
 
5
18
  - Fix `MigrationRunner` to consider `run_background_migrations_inline` proc
data/README.md CHANGED
@@ -40,11 +40,11 @@ $ bin/rails generate online_migrations:install
40
40
  $ bin/rails db:migrate
41
41
  ```
42
42
 
43
- **Note**: If you do not have plans on using [background migrations](docs/background_migrations.md) feature, then you can delete the generated migration and regenerate it later, if needed.
43
+ **Note**: If you do not have plans on using [background data migrations](docs/background_data_migrations.md) or [background schema migrations](docs/background_schema_migrations.md) features, then you can delete the generated migration and regenerate it later, if needed.
44
44
 
45
45
  ### Upgrading
46
46
 
47
- If you're already using [background migrations](docs/background_migrations.md), your background migrations tables may require additional columns. After every upgrade run:
47
+ If you're already using [background data migrations](docs/background_data_migrations.md) or [background schema migrations](docs/background_schema_migrations.md), your background migrations tables may require additional columns. After every upgrade run:
48
48
 
49
49
  ```sh
50
50
  $ bin/rails generate online_migrations:upgrade
@@ -191,7 +191,7 @@ end
191
191
 
192
192
  ```ruby
193
193
  class User < ApplicationRecord
194
- self.ignored_columns = ["name"]
194
+ self.ignored_columns += ["name"]
195
195
  end
196
196
  ```
197
197
 
@@ -285,7 +285,7 @@ end
285
285
  ```
286
286
 
287
287
  **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
288
- **Note**: You may consider [background migrations](#background-migrations) to run data changes on large tables.
288
+ **Note**: You may consider [background data migrations](#background-data-migrations) or [background schema migrations](#background-schema-migrations) to run data changes on large tables.
289
289
 
290
290
  ### Changing the type of a column
291
291
 
@@ -488,7 +488,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
488
488
 
489
489
  ```ruby
490
490
  class User < ApplicationRecord
491
- self.ignored_columns = ["name"]
491
+ self.ignored_columns += ["name"]
492
492
  end
493
493
  ```
494
494
 
@@ -1172,7 +1172,7 @@ A safer approach is to:
1172
1172
 
1173
1173
  ```ruby
1174
1174
  class User < ApplicationRecord
1175
- self.ignored_columns = ["type"]
1175
+ self.ignored_columns += ["type"]
1176
1176
  end
1177
1177
  ```
1178
1178
 
@@ -1228,9 +1228,13 @@ Certain methods like `execute` and `change_table` cannot be inspected and are pr
1228
1228
 
1229
1229
  Read [configuring.md](docs/configuring.md).
1230
1230
 
1231
- ## Background Migrations
1231
+ ## Background Data Migrations
1232
1232
 
1233
- Read [background_migrations.md](docs/background_migrations.md) on how to perform data migrations on large tables.
1233
+ Read [background_data_migrations.md](docs/background_data_migrations.md) on how to perform data migrations on large tables.
1234
+
1235
+ ## Background Schema Migrations
1236
+
1237
+ Read [background_schema_migrations.md](docs/background_schema_migrations.md) on how to perform background schema migrations on large tables.
1234
1238
 
1235
1239
  ## Credits
1236
1240
 
@@ -1295,7 +1299,7 @@ The main differences are:
1295
1299
  * adding different types of constraints
1296
1300
  * and others
1297
1301
 
1298
- 2. This gem has a [powerful internal framework](https://github.com/fatkodima/online_migrations/blob/master/docs/background_migrations.md) for running data migrations on very large tables using background migrations.
1302
+ 2. This gem has a powerful internal framework for running [data migrations](docs/background_data_migrations.md) and [schema migrations](docs/background_schema_migrations.md) on very large tables in background.
1299
1303
 
1300
1304
  For example, you can use background migrations to migrate data that’s stored in a single JSON column to a separate table instead; backfill values from one column to another (as one of the steps when changing column type); or backfill some column’s value from an API.
1301
1305
 
@@ -1,4 +1,4 @@
1
- # Background Migrations
1
+ # Background Data Migrations
2
2
 
3
3
  When a project grows, your database starts to be heavy and changing the data through the deployment process can be very painful.
4
4
 
@@ -18,7 +18,7 @@ Start a background migrations scheduler. For example, to run it on cron using [w
18
18
 
19
19
  ```ruby
20
20
  every 1.minute do
21
- runner "OnlineMigrations.run_background_migrations"
21
+ runner "OnlineMigrations.run_background_data_migrations"
22
22
  end
23
23
  ```
24
24
 
@@ -30,7 +30,8 @@ A generator is provided to create background migrations. Generate a new backgrou
30
30
  $ bin/rails generate online_migrations:background_migration backfill_project_issues_count
31
31
  ```
32
32
 
33
- This creates the background migration file `lib/online_migrations/background_migrations/backfill_project_issues_count.rb`.
33
+ This creates the background migration file `lib/online_migrations/background_migrations/backfill_project_issues_count.rb`
34
+ and the regular migration file `db/migrate/xxxxxxxxxxxxxx_enqueue_backfill_project_issues_count.rb` where we enqueue it.
34
35
 
35
36
  The generated class is a subclass of `OnlineMigrations::BackgroundMigration` that implements:
36
37
 
@@ -76,12 +77,16 @@ end
76
77
  You can enqueue your background migration to be run by the scheduler via:
77
78
 
78
79
  ```ruby
79
- # db/migrate/xxxxxxxxxxxxxx_backfill_project_issues_count.rb
80
- # ...
81
- def up
82
- enqueue_background_migration("BackfillProjectIssuesCount")
80
+ # db/migrate/xxxxxxxxxxxxxx_enqueue_backfill_project_issues_count.rb
81
+ class EnqueueBackfillProjectIssuesCount < ActiveRecord::Migration[7.1]
82
+ def up
83
+ enqueue_background_migration("BackfillProjectIssuesCount")
84
+ end
85
+
86
+ def down
87
+ remove_background_migration("BackfillProjectIssuesCount")
88
+ end
83
89
  end
84
- # ...
85
90
  ```
86
91
 
87
92
  `enqueue_background_migration` accepts additional configuration options which controls how the background migration is run. Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/background_migrations/migration_helpers.rb) for the list of all available configuration options.
@@ -106,7 +111,17 @@ end
106
111
  And pass them when enqueuing:
107
112
 
108
113
  ```ruby
109
- enqueue_background_migration("MyMigrationWithArgs", arg1, arg2, ...)
114
+ def up
115
+ enqueue_background_migration("MyMigrationWithArgs", arg1, arg2, ...)
116
+ end
117
+ ```
118
+
119
+ Make sure to also pass the arguments inside the `down` method of the migration:
120
+
121
+ ```ruby
122
+ def down
123
+ remove_background_migration("MyMigrationWithArgs", arg1, arg2, ...)
124
+ end
110
125
  ```
111
126
 
112
127
  ## Considerations when writing Background Migrations
@@ -246,20 +261,6 @@ migration.update!(
246
261
  )
247
262
  ```
248
263
 
249
- ### Throttling
250
-
251
- Background Migrations often modify a lot of data and can be taxing on your database. There is a throttling mechanism that can be used to throttle a background migration when a given condition is met. If a migration is throttled, it will be interrupted and retried on the next Scheduler cycle run.
252
-
253
- Specify the throttle condition as a block:
254
-
255
- ```ruby
256
- # config/initializers/online_migrations.rb
257
-
258
- config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
259
- ```
260
-
261
- Note that it's up to you to define a throttling condition that makes sense for your app. For example, you can check various PostgreSQL metrics such as replication lag, DB threads, whether DB writes are available, etc.
262
-
263
264
  ### Customizing the error handler
264
265
 
265
266
  Exceptions raised while a Background Migration is performing are rescued and information about the error is persisted in the database.
@@ -306,21 +307,6 @@ config.background_migrations.migrations_module = "BackgroundMigrationsModule"
306
307
 
307
308
  If no value is specified, it will default to `"OnlineMigrations::BackgroundMigrations"`.
308
309
 
309
- ### Customizing the backtrace cleaner
310
-
311
- `config.background_migrations.backtrace_cleaner` can be configured to specify a backtrace cleaner to use when a Background Migration errors and the backtrace is cleaned and persisted. An `ActiveSupport::BacktraceCleaner` should be used.
312
-
313
- ```ruby
314
- # config/initializers/online_migrations.rb
315
-
316
- cleaner = ActiveSupport::BacktraceCleaner.new
317
- cleaner.add_silencer { |line| line =~ /ignore_this_dir/ }
318
-
319
- config.background_migrations.backtrace_cleaner = cleaner
320
- ```
321
-
322
- If none is specified, the default `Rails.backtrace_cleaner` will be used to clean backtraces.
323
-
324
310
  ### Multiple databases and sharding
325
311
 
326
312
  If you have multiple databases or sharding, you may need to configure where background migrations related tables live
@@ -330,10 +316,10 @@ by configuring the parent model:
330
316
  # config/initializers/online_migrations.rb
331
317
 
332
318
  # Referring to one of the databases
333
- OnlineMigrations::BackgroundMigrations::ApplicationRecord.connects_to database: { writing: :animals }
319
+ OnlineMigrations::ApplicationRecord.connects_to database: { writing: :animals }
334
320
 
335
321
  # Referring to one of the shards (via `:database` option)
336
- OnlineMigrations::BackgroundMigrations::ApplicationRecord.connects_to database: { writing: :shard_one }
322
+ OnlineMigrations::ApplicationRecord.connects_to database: { writing: :shard_one }
337
323
  ```
338
324
 
339
325
  By default, ActiveRecord uses the database config named `:primary` (if exists) under the environment section from the `database.yml`.
@@ -0,0 +1,163 @@
1
+ # Background Schema Migrations
2
+
3
+ When a project grows, your database starts to be heavy and performing schema changes through the deployment process can be very painful.
4
+
5
+ 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.
6
+
7
+ **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.
8
+
9
+ ## Installation
10
+
11
+ Make sure you have migration files generated when installed this gem:
12
+
13
+ ```sh
14
+ $ bin/rails generate online_migrations:install
15
+ ```
16
+
17
+ 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:
18
+
19
+ ```ruby
20
+ every 1.minute do
21
+ runner "OnlineMigrations.run_background_schema_migrations"
22
+ end
23
+ ```
24
+
25
+ or run it manually when the deployment is finished, from the rails console:
26
+
27
+ ```rb
28
+ [production] (main)> OnlineMigrations.run_background_schema_migrations
29
+ ```
30
+
31
+ **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.
32
+
33
+ **Note**: Make sure that the process that runs the scheduler does not die until the migration is finished.
34
+
35
+ ## Enqueueing a Background Schema Migration
36
+
37
+ Currently, only helpers for adding/removing indexes are provided.
38
+
39
+ Background schema migrations should be performed in 2 steps:
40
+
41
+ 1. Create a PR that schedules the index to be created/removed
42
+ 2. Verify that the PR was deployed and that the index was actually created/removed on production.
43
+ 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`
44
+
45
+ To schedule an index creation:
46
+
47
+ ```ruby
48
+ # db/migrate/xxxxxxxxxxxxxx_add_index_to_users_email_in_background.rb
49
+ def up
50
+ add_index_in_background(:users, :email, unique: true)
51
+ end
52
+ ```
53
+
54
+ To schedule an index removal:
55
+
56
+ ```ruby
57
+ # db/migrate/xxxxxxxxxxxxxx_remove_index_from_users_email_in_background.rb
58
+ def up
59
+ remove_index_in_background(:users, name: "index_users_on_email")
60
+ end
61
+ ```
62
+
63
+ `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.
64
+
65
+ ## Instrumentation
66
+
67
+ Background schema migrations use the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API.
68
+
69
+ You can subscribe to `background_schema_migrations` events and log it, graph it, etc.
70
+
71
+ 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:
72
+
73
+ ```ruby
74
+ # config/initializers/online_migrations.rb
75
+ ActiveSupport::Notifications.subscribe("retried.background_schema_migrations") do |name, start, finish, id, payload|
76
+ # background schema migration object is available in payload[:background_schema_migration]
77
+
78
+ # Your code here
79
+ end
80
+ ```
81
+
82
+ If you want to subscribe to every `background_schema_migrations` event, use:
83
+
84
+ ```ruby
85
+ # config/initializers/online_migrations.rb
86
+ ActiveSupport::Notifications.subscribe(/background_schema_migrations/) do |name, start, finish, id, payload|
87
+ # background schema migration object is available in payload[:background_schema_migration]
88
+
89
+ # Your code here
90
+ end
91
+ ```
92
+
93
+ Available events:
94
+
95
+ * `started.background_schema_migrations`
96
+ * `run.background_schema_migrations`
97
+ * `completed.background_schema_migrations`
98
+ * `retried.background_schema_migrations`
99
+ * `throttled.background_schema_migrations`
100
+
101
+ ## Monitoring Background Schema Migrations
102
+
103
+ Background Schema Migrations can be in various states during its execution:
104
+
105
+ * **enqueued**: A migration has been enqueued by the user.
106
+ * **running**: A migration is being performed by a migration executor.
107
+ * **failed**: A migration raises an exception when running.
108
+ * **succeeded**: A migration finished without error.
109
+
110
+ ## Configuring
111
+
112
+ There are a few configurable options for the Background Schema Migrations. Custom configurations should be placed in a `online_migrations.rb` initializer.
113
+
114
+ 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.
115
+
116
+ **Note**: You can dynamically change certain migration parameters while the migration is run.
117
+ For example,
118
+ ```ruby
119
+ migration = OnlineMigrations::BackgroundSchemaMigrations::Migration.find(id)
120
+ migration.update!(
121
+ statement_timeout: 2.hours, # The statement timeout value used when running the migration
122
+ max_attempts: 10 # The # of attempts the failing migration will be retried
123
+ )
124
+ ```
125
+
126
+ ### Customizing the error handler
127
+
128
+ Exceptions raised while a Background Schema Migration is performing are rescued and information about the error is persisted in the database.
129
+
130
+ If you want to integrate with an exception monitoring service (e.g. Bugsnag), you can define an error handler:
131
+
132
+ ```ruby
133
+ # config/initializers/online_migrations.rb
134
+
135
+ OnlineMigrations.config.background_schema_migrations.error_handler = ->(error, errored_migration) do
136
+ Bugsnag.notify(error) do |notification|
137
+ notification.add_metadata(:background_schema_migration, { name: errored_migration.name })
138
+ end
139
+ end
140
+ ```
141
+
142
+ The error handler should be a lambda that accepts 2 arguments:
143
+
144
+ * `error`: The exception that was raised.
145
+ * `errored_migration`: An `OnlineMigrations::BackgroundSchemaMigrations::Migration` object that represents a failed migration.
146
+
147
+ ### Multiple databases and sharding
148
+
149
+ If you have multiple databases or sharding, you may need to configure where background migrations related tables live
150
+ by configuring the parent model:
151
+
152
+ ```ruby
153
+ # config/initializers/online_migrations.rb
154
+
155
+ # Referring to one of the databases
156
+ OnlineMigrations::ApplicationRecord.connects_to database: { writing: :animals }
157
+
158
+ # Referring to one of the shards (via `:database` option)
159
+ OnlineMigrations::ApplicationRecord.connects_to database: { writing: :shard_one }
160
+ ```
161
+
162
+ By default, ActiveRecord uses the database config named `:primary` (if exists) under the environment section from the `database.yml`.
163
+ Otherwise, the first config under the environment section is used.
data/docs/configuring.md CHANGED
@@ -101,13 +101,15 @@ config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
101
101
  )
102
102
  ```
103
103
 
104
- When statement within transaction fails - the whole transaction is retried.
104
+ When a statement within transaction fails - the whole transaction is retried. If any statement fails when running outside a transaction (e.g. using `disable_ddl_transaction!`) then only that statement is retried.
105
105
 
106
- To permanently disable lock retries, you can set `lock_retrier` to `nil`.
106
+ **Note**: Statements are retried by default, unless lock retries are disabled. It is possible to implement more sophisticated lock retriers. See [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/lock_retrier.rb) for the examples.
107
107
 
108
108
  To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_RETRIES` env variable. This is useful when you are deploying a hotfix and do not want to wait too long while the lock retrier safely tries to acquire the lock, but try to acquire the lock immediately with the default configured lock timeout value.
109
109
 
110
- **Note**: Statements are retried by default, unless lock retries are disabled. It is possible to implement more sophisticated lock retriers. See [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/lock_retrier.rb) for the examples.
110
+ To permanently disable lock retries, you can set `lock_retrier` to `nil`.
111
+
112
+ Finally, if your lock retrier implementation does not have an explicit `lock_timeout` value configured, then the timeout behavior will fallback to the database configuration (`config/database.yml`) or the PostgreSQL server config value ([off by default](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT)). Take care configuring this value, as this fallback may result in your migrations running without a lock timeout!
111
113
 
112
114
  ## Existing Migrations
113
115
 
@@ -214,7 +216,7 @@ Add to an initializer file:
214
216
  config.auto_analyze = true
215
217
  ```
216
218
 
217
- ### Running background migrations inline
219
+ ## Running background migrations inline
218
220
 
219
221
  `config.run_background_migrations_inline` can be configured with a proc to decide whether background migrations should be run inline. For convenience defaults to true for development and test environments.
220
222
 
@@ -225,6 +227,35 @@ config.run_background_migrations_inline = -> { Rails.env.local? }
225
227
 
226
228
  Set to `nil` to avoid running background migrations inline.
227
229
 
230
+ ## Throttling
231
+
232
+ Background data and schema migrations can be taxing on your database. There is a throttling mechanism that can be used to throttle a background migration when a given condition is met. If a migration is throttled, it will be interrupted and retried on the next Scheduler cycle run.
233
+
234
+ Specify the throttle condition as a block:
235
+
236
+ ```ruby
237
+ # config/initializers/online_migrations.rb
238
+
239
+ OnlineMigrations.config.throttler = -> { DatabaseStatus.unhealthy? }
240
+ ```
241
+
242
+ **Note**: It's up to you to define a throttling condition that makes sense for your app. For example, you can check various PostgreSQL metrics such as replication lag, DB threads, whether DB writes are available, etc.
243
+
244
+ ## Customizing the backtrace cleaner
245
+
246
+ `OnlineMigrations.config.backtrace_cleaner` can be configured to specify a backtrace cleaner to use when a background data or schema migration errors and the backtrace is cleaned and persisted. An `ActiveSupport::BacktraceCleaner` should be used.
247
+
248
+ ```ruby
249
+ # config/initializers/online_migrations.rb
250
+
251
+ cleaner = ActiveSupport::BacktraceCleaner.new
252
+ cleaner.add_silencer { |line| line =~ /ignore_this_dir/ }
253
+
254
+ OnlineMigrations.config.backtrace_cleaner = cleaner
255
+ ```
256
+
257
+ If none is specified, the default `Rails.backtrace_cleaner` will be used to clean backtraces.
258
+
228
259
  ## Schema Sanity
229
260
 
230
261
  Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/).
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "rails/generators/active_record/migration"
4
5
 
5
6
  module OnlineMigrations
6
7
  # @private
7
8
  class BackgroundMigrationGenerator < Rails::Generators::NamedBase
9
+ include ActiveRecord::Generators::Migration
10
+
8
11
  source_root File.expand_path("templates", __dir__)
9
- desc "This generator creates a background migration file."
12
+ desc "This generator creates a background migration related files."
10
13
 
11
14
  def create_background_migration_file
12
15
  migrations_module_file_path = migrations_module.underscore
@@ -20,6 +23,10 @@ module OnlineMigrations
20
23
  template("background_migration.rb", template_file)
21
24
  end
22
25
 
26
+ def create_migration_file
27
+ migration_template("migration.rb", File.join(db_migrate_path, "enqueue_#{file_name}.rb"))
28
+ end
29
+
23
30
  private
24
31
  def migrations_module
25
32
  config.migrations_module
@@ -28,5 +35,9 @@ module OnlineMigrations
28
35
  def config
29
36
  OnlineMigrations.config.background_migrations
30
37
  end
38
+
39
+ def migration_parent
40
+ "ActiveRecord::Migration[#{Utils.ar_version}]"
41
+ end
31
42
  end
32
43
  end
@@ -15,7 +15,7 @@ module OnlineMigrations
15
15
  end
16
16
 
17
17
  def create_migration_file
18
- migration_template("migration.rb", File.join(db_migrate_path, "install_online_migrations.rb"))
18
+ migration_template("install_migration.rb", File.join(db_migrate_path, "install_online_migrations.rb"))
19
19
  end
20
20
 
21
21
  private
@@ -0,0 +1,29 @@
1
+ class CreateBackgroundSchemaMigrations < <%= migration_parent %>
2
+ def change
3
+ # You can remove this migration for now and regenerate it later if you do not have plans
4
+ # to use background schema migrations, like adding indexes in the background.
5
+ create_table :background_schema_migrations do |t|
6
+ t.bigint :parent_id
7
+ t.string :migration_name, null: false
8
+ t.string :table_name, null: false
9
+ t.string :definition, null: false
10
+ t.string :status, default: "enqueued", null: false
11
+ t.string :shard
12
+ t.boolean :composite, default: false, null: false
13
+ t.integer :statement_timeout
14
+ t.datetime :started_at
15
+ t.datetime :finished_at
16
+ t.integer :max_attempts, null: false
17
+ t.integer :attempts, default: 0, null: false
18
+ t.string :error_class
19
+ t.string :error_message
20
+ t.string :backtrace, array: true
21
+ t.string :connection_class_name
22
+ t.timestamps
23
+
24
+ t.foreign_key :background_schema_migrations, column: :parent_id, on_delete: :cascade
25
+
26
+ t.index [:migration_name, :shard], unique: true, name: :index_background_schema_migrations_on_unique_configuration
27
+ end
28
+ end
29
+ end
@@ -77,12 +77,20 @@ OnlineMigrations.configure do |config|
77
77
  # end
78
78
  # end
79
79
 
80
- # Decide whether background migrations should be run inline.
80
+ # Decide whether background data and schema migrations should be run inline.
81
81
  # Convenient for development and test environments.
82
82
  config.run_background_migrations_inline = -> { Rails.env.local? }
83
83
 
84
- # ==> Background migrations configuration
84
+ # Configure custom throttler for background data and schema migrations.
85
+ # It will be called before each run.
86
+ # If throttled, the current run will be retried next time.
87
+ # config.throttler = -> { DatabaseStatus.unhealthy? }
88
+
89
+ # The Active Support backtrace cleaner that will be used to clean the
90
+ # backtrace of a background data or schema migration that errors.
91
+ config.backtrace_cleaner = Rails.backtrace_cleaner
85
92
 
93
+ # ==> Background data migrations configuration
86
94
  # The path where generated background migrations will be placed.
87
95
  # config.background_migrations.migrations_path = "lib"
88
96
 
@@ -105,22 +113,27 @@ OnlineMigrations.configure do |config|
105
113
  # When attempts are exhausted, the individual batch is marked as failed.
106
114
  # config.background_migrations.batch_max_attempts = 5
107
115
 
108
- # Configure custom throttler for background migrations.
109
- # It will be called before each batch run.
110
- # If throttled, the current run will be retried next time.
111
- # config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
112
-
113
116
  # The number of seconds that must pass before the running job is considered stuck.
114
117
  # config.background_migrations.stuck_jobs_timeout = 1.hour
115
118
 
116
- # The Active Support backtrace cleaner that will be used to clean the
117
- # backtrace of a migration job that errors.
118
- config.background_migrations.backtrace_cleaner = Rails.backtrace_cleaner
119
-
120
119
  # The callback to perform when an error occurs in the migration job.
121
120
  # config.background_migrations.error_handler = ->(error, errored_job) do
122
121
  # Bugsnag.notify(error) do |notification|
123
122
  # notification.add_metadata(:background_migration, { name: errored_job.migration_name })
124
123
  # end
125
124
  # end
125
+
126
+ # ==> Background schema migrations configuration
127
+ # When attempts are exhausted, the failing migration stops to be retried.
128
+ # config.background_schema_migrations.max_attempts = 5
129
+
130
+ # Statement timeout value used when running background schema migration.
131
+ # config.background_schema_migrations.statement_timeout = 1.hour
132
+
133
+ # The callback to perform when an error occurs during the background schema migration.
134
+ # config.background_schema_migrations.error_handler = ->(error, errored_migration) do
135
+ # Bugsnag.notify(error) do |notification|
136
+ # notification.add_metadata(:background_schema_migration, { name: errored_migration.name })
137
+ # end
138
+ # end
126
139
  end
@@ -0,0 +1,75 @@
1
+ class InstallOnlineMigrations < <%= migration_parent %>
2
+ def change
3
+ create_table :background_migrations do |t|
4
+ t.bigint :parent_id
5
+ t.string :migration_name, null: false
6
+ t.jsonb :arguments, default: [], null: false
7
+ t.string :batch_column_name, null: false
8
+ t.bigint :min_value, null: false
9
+ t.bigint :max_value, null: false
10
+ t.bigint :rows_count
11
+ t.integer :batch_size, null: false
12
+ t.integer :sub_batch_size, null: false
13
+ t.integer :batch_pause, null: false
14
+ t.integer :sub_batch_pause_ms, null: false
15
+ t.integer :batch_max_attempts, null: false
16
+ t.string :status, default: "enqueued", null: false
17
+ t.string :shard
18
+ t.boolean :composite, default: false, null: false
19
+ t.timestamps
20
+
21
+ t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
22
+
23
+ t.index [:migration_name, :arguments, :shard],
24
+ unique: true, name: :index_background_migrations_on_unique_configuration
25
+ end
26
+
27
+ create_table :background_migration_jobs do |t|
28
+ t.bigint :migration_id, null: false
29
+ t.bigint :min_value, null: false
30
+ t.bigint :max_value, null: false
31
+ t.integer :batch_size, null: false
32
+ t.integer :sub_batch_size, null: false
33
+ t.integer :pause_ms, null: false
34
+ t.datetime :started_at
35
+ t.datetime :finished_at
36
+ t.string :status, default: "enqueued", null: false
37
+ t.integer :max_attempts, null: false
38
+ t.integer :attempts, default: 0, null: false
39
+ t.string :error_class
40
+ t.string :error_message
41
+ t.string :backtrace, array: true
42
+ t.timestamps
43
+
44
+ t.foreign_key :background_migrations, column: :migration_id, on_delete: :cascade
45
+
46
+ t.index [:migration_id, :max_value], name: :index_background_migration_jobs_on_max_value
47
+ t.index [:migration_id, :status, :updated_at], name: :index_background_migration_jobs_on_updated_at
48
+ t.index [:migration_id, :finished_at], name: :index_background_migration_jobs_on_finished_at
49
+ end
50
+
51
+ create_table :background_schema_migrations do |t|
52
+ t.bigint :parent_id
53
+ t.string :migration_name, null: false
54
+ t.string :table_name, null: false
55
+ t.string :definition, null: false
56
+ t.string :status, default: "enqueued", null: false
57
+ t.string :shard
58
+ t.boolean :composite, default: false, null: false
59
+ t.integer :statement_timeout
60
+ t.datetime :started_at
61
+ t.datetime :finished_at
62
+ t.integer :max_attempts, null: false
63
+ t.integer :attempts, default: 0, null: false
64
+ t.string :error_class
65
+ t.string :error_message
66
+ t.string :backtrace, array: true
67
+ t.string :connection_class_name
68
+ t.timestamps
69
+
70
+ t.foreign_key :background_schema_migrations, column: :parent_id, on_delete: :cascade
71
+
72
+ t.index [:migration_name, :shard], unique: true, name: :index_background_schema_migrations_on_unique_configuration
73
+ end
74
+ end
75
+ end