online_migrations 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +155 -150
- data/docs/background_migrations.md +43 -10
- data/docs/configuring.md +23 -18
- data/lib/generators/online_migrations/install_generator.rb +3 -7
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +12 -3
- data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
- data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
- data/lib/online_migrations/background_migrations/application_record.rb +13 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
- data/lib/online_migrations/background_migrations/copy_column.rb +11 -19
- data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
- data/lib/online_migrations/background_migrations/migration.rb +123 -34
- data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
- data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
- data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
- data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
- data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
- data/lib/online_migrations/change_column_type_helpers.rb +71 -86
- data/lib/online_migrations/command_checker.rb +50 -46
- data/lib/online_migrations/config.rb +19 -15
- data/lib/online_migrations/copy_trigger.rb +15 -10
- data/lib/online_migrations/error_messages.rb +13 -25
- data/lib/online_migrations/foreign_keys_collector.rb +2 -2
- data/lib/online_migrations/indexes_collector.rb +3 -3
- data/lib/online_migrations/lock_retrier.rb +4 -9
- data/lib/online_migrations/schema_cache.rb +0 -6
- data/lib/online_migrations/schema_dumper.rb +21 -0
- data/lib/online_migrations/schema_statements.rb +80 -256
- data/lib/online_migrations/utils.rb +36 -55
- data/lib/online_migrations/verbose_sql_logs.rb +3 -2
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +9 -6
- metadata +9 -7
- data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
- data/lib/online_migrations/foreign_key_definition.rb +0 -17
data/docs/configuring.md
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
There are a few configurable options for the gem. Custom configurations should be placed in a `online_migrations.rb` initializer.
|
4
4
|
|
5
5
|
```ruby
|
6
|
+
# config/initializers/online_migrations.rb
|
7
|
+
|
6
8
|
OnlineMigrations.configure do |config|
|
7
9
|
# ...
|
8
10
|
end
|
@@ -15,8 +17,6 @@ end
|
|
15
17
|
Add your own custom checks with:
|
16
18
|
|
17
19
|
```ruby
|
18
|
-
# config/initializers/online_migrations.rb
|
19
|
-
|
20
20
|
config.add_check do |method, args|
|
21
21
|
if method == :add_column && args[0].to_s == "users"
|
22
22
|
stop!("No more columns on the users table")
|
@@ -33,8 +33,6 @@ Use the `stop!` method to stop migrations.
|
|
33
33
|
Disable specific checks with:
|
34
34
|
|
35
35
|
```ruby
|
36
|
-
# config/initializers/online_migrations.rb
|
37
|
-
|
38
36
|
config.disable_check(:remove_index)
|
39
37
|
```
|
40
38
|
|
@@ -45,8 +43,6 @@ Check the [source code](https://github.com/fatkodima/online_migrations/blob/mast
|
|
45
43
|
By default, checks are disabled when migrating down. Enable them with:
|
46
44
|
|
47
45
|
```ruby
|
48
|
-
# config/initializers/online_migrations.rb
|
49
|
-
|
50
46
|
config.check_down = true
|
51
47
|
```
|
52
48
|
|
@@ -55,8 +51,6 @@ config.check_down = true
|
|
55
51
|
You can customize specific error messages:
|
56
52
|
|
57
53
|
```ruby
|
58
|
-
# config/initializers/online_migrations.rb
|
59
|
-
|
60
54
|
config.error_messages[:add_column_default] = "Your custom instructions"
|
61
55
|
```
|
62
56
|
|
@@ -88,20 +82,19 @@ ALTER ROLE myuser SET statement_timeout = '15s';
|
|
88
82
|
You can configure this gem to automatically retry statements that exceed the lock timeout:
|
89
83
|
|
90
84
|
```ruby
|
91
|
-
# config/initializers/online_migrations.rb
|
92
|
-
|
93
85
|
config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
|
94
86
|
attempts: 30, # attempt 30 retries
|
95
87
|
base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
|
96
88
|
max_delay: 1.minute, # maximum delay is 1 minute
|
97
|
-
lock_timeout: 0.
|
89
|
+
lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try
|
98
90
|
)
|
99
91
|
```
|
100
92
|
|
101
93
|
When statement within transaction fails - the whole transaction is retried.
|
102
94
|
|
103
95
|
To permanently disable lock retries, you can set `lock_retrier` to `nil`.
|
104
|
-
|
96
|
+
|
97
|
+
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.
|
105
98
|
|
106
99
|
**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
100
|
|
@@ -110,8 +103,6 @@ To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_
|
|
110
103
|
To mark migrations as safe that were created before installing this gem, configure the migration version starting after which checks are performed:
|
111
104
|
|
112
105
|
```ruby
|
113
|
-
# config/initializers/online_migrations.rb
|
114
|
-
|
115
106
|
config.start_after = 20220101000000
|
116
107
|
|
117
108
|
# or if you use multiple databases (Active Record 6+)
|
@@ -125,8 +116,6 @@ Use the version from your latest migration.
|
|
125
116
|
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
126
117
|
|
127
118
|
```ruby
|
128
|
-
# config/initializers/online_migrations.rb
|
129
|
-
|
130
119
|
config.target_version = 10 # or "12.9" etc
|
131
120
|
|
132
121
|
# or if you use multiple databases (Active Record 6+)
|
@@ -200,9 +189,25 @@ So you can actually check which steps are performed.
|
|
200
189
|
To enable verbose sql logs:
|
201
190
|
|
202
191
|
```ruby
|
203
|
-
# config/initializers/online_migrations.rb
|
204
|
-
|
205
192
|
config.verbose_sql_logs = true
|
206
193
|
```
|
207
194
|
|
208
195
|
This feature is enabled by default in a production Rails environment. You can override this setting via `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
196
|
+
|
197
|
+
## Analyze Tables
|
198
|
+
|
199
|
+
Analyze tables automatically (to update planner statistics) after an index is added.
|
200
|
+
Add to an initializer file:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
config.auto_analyze = true
|
204
|
+
```
|
205
|
+
|
206
|
+
## Schema Sanity
|
207
|
+
|
208
|
+
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/).
|
209
|
+
To alphabetize columns:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
config.alphabetize_schema = true
|
213
|
+
```
|
@@ -15,20 +15,16 @@ module OnlineMigrations
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def create_migration_file
|
18
|
-
migration_template("migration.rb", File.join(
|
18
|
+
migration_template("migration.rb", File.join(db_migrate_path, "install_online_migrations.rb"))
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
22
22
|
def migration_parent
|
23
|
-
Utils.
|
23
|
+
"ActiveRecord::Migration[#{Utils.ar_version}]"
|
24
24
|
end
|
25
25
|
|
26
26
|
def start_after
|
27
|
-
self.class.current_migration_number(
|
28
|
-
end
|
29
|
-
|
30
|
-
def migrations_dir
|
31
|
-
Utils.ar_version >= 5.1 ? db_migrate_path : "db/migrate"
|
27
|
+
self.class.current_migration_number(db_migrate_path)
|
32
28
|
end
|
33
29
|
end
|
34
30
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class AddShardingToOnlineMigrations < <%= migration_parent %>
|
2
|
+
def change
|
3
|
+
safety_assured do
|
4
|
+
remove_index :background_migrations, [:migration_name, :arguments], unique: true
|
5
|
+
|
6
|
+
change_table :background_migrations do |t|
|
7
|
+
t.bigint :parent_id
|
8
|
+
t.string :shard
|
9
|
+
t.boolean :composite, default: false, null: false
|
10
|
+
|
11
|
+
t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
|
12
|
+
|
13
|
+
t.index [:migration_name, :arguments, :shard],
|
14
|
+
unique: true, name: :index_background_migrations_on_unique_configuration
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -23,13 +23,22 @@ OnlineMigrations.configure do |config|
|
|
23
23
|
# It is considered safe to perform most of the dangerous operations on them.
|
24
24
|
# config.small_tables = []
|
25
25
|
|
26
|
+
# Analyze tables after indexes are added.
|
27
|
+
# Outdated statistics can sometimes hurt performance.
|
28
|
+
# config.auto_analyze = true
|
29
|
+
|
30
|
+
# Alphabetize table columns when dumping the schema.
|
31
|
+
# config.alphabetize_schema = true
|
32
|
+
|
26
33
|
# Disable specific checks.
|
27
|
-
# For the list of available checks look at `
|
34
|
+
# For the list of available checks look at the `error_messages.rb` file inside
|
35
|
+
# the `online_migrations` gem.
|
28
36
|
# config.disable_check(:remove_index)
|
29
37
|
|
30
38
|
# Enable specific checks. All checks are enabled by default,
|
31
39
|
# but this may change in the future.
|
32
|
-
# For the list of available checks look at `
|
40
|
+
# For the list of available checks look at the `error_messages.rb` file inside
|
41
|
+
# the `online_migrations` gem.
|
33
42
|
# config.enable_check(:remove_index)
|
34
43
|
|
35
44
|
# Configure whether to log every SQL query happening in a migration.
|
@@ -48,7 +57,7 @@ OnlineMigrations.configure do |config|
|
|
48
57
|
attempts: 30, # attempt 30 retries
|
49
58
|
base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
|
50
59
|
max_delay: 1.minute, # up to the maximum delay of 1 minute
|
51
|
-
lock_timeout: 0.
|
60
|
+
lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try
|
52
61
|
)
|
53
62
|
|
54
63
|
# Configure tables that are in the process of being renamed.
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class InstallOnlineMigrations < <%= migration_parent %>
|
2
2
|
def change
|
3
3
|
create_table :background_migrations do |t|
|
4
|
+
t.bigint :parent_id
|
4
5
|
t.string :migration_name, null: false
|
5
6
|
t.jsonb :arguments, default: [], null: false
|
6
7
|
t.string :batch_column_name, null: false
|
@@ -13,9 +14,13 @@ class InstallOnlineMigrations < <%= migration_parent %>
|
|
13
14
|
t.integer :sub_batch_pause_ms, null: false
|
14
15
|
t.integer :batch_max_attempts, null: false
|
15
16
|
t.string :status, default: "enqueued", null: false
|
16
|
-
t.
|
17
|
+
t.string :shard
|
18
|
+
t.boolean :composite, default: false, null: false
|
19
|
+
t.timestamps
|
17
20
|
|
18
|
-
t.
|
21
|
+
t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
|
22
|
+
|
23
|
+
t.index [:migration_name, :arguments, :shard],
|
19
24
|
unique: true, name: :index_background_migrations_on_unique_configuration
|
20
25
|
end
|
21
26
|
|
@@ -34,7 +39,7 @@ class InstallOnlineMigrations < <%= migration_parent %>
|
|
34
39
|
t.string :error_class
|
35
40
|
t.string :error_message
|
36
41
|
t.string :backtrace, array: true
|
37
|
-
t.timestamps
|
42
|
+
t.timestamps
|
38
43
|
|
39
44
|
t.foreign_key :background_migrations, column: :migration_id, on_delete: :cascade
|
40
45
|
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record/migration"
|
5
|
+
|
6
|
+
module OnlineMigrations
|
7
|
+
# @private
|
8
|
+
class UpgradeGenerator < Rails::Generators::Base
|
9
|
+
include ActiveRecord::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
def copy_templates
|
14
|
+
migrations_to_be_applied.each do |migration|
|
15
|
+
migration_template("#{migration}.rb", File.join(db_migrate_path, "#{migration}.rb"))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def migrations_to_be_applied
|
21
|
+
connection = BackgroundMigrations::Migration.connection
|
22
|
+
columns = connection.columns(BackgroundMigrations::Migration.table_name).map(&:name)
|
23
|
+
|
24
|
+
migrations = []
|
25
|
+
migrations << "add_sharding_to_online_migrations" if !columns.include?("shard")
|
26
|
+
migrations
|
27
|
+
end
|
28
|
+
|
29
|
+
def migration_parent
|
30
|
+
"ActiveRecord::Migration[#{Utils.ar_version}]"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundMigrations
|
5
|
+
# Base class for all records used by this gem.
|
6
|
+
#
|
7
|
+
# Can be extended to setup different database where all tables related to
|
8
|
+
# online_migrations will live.
|
9
|
+
class ApplicationRecord < ActiveRecord::Base
|
10
|
+
self.abstract_class = true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -22,7 +22,7 @@ module OnlineMigrations
|
|
22
22
|
quoted_column = connection.quote_column_name(column)
|
23
23
|
model.unscoped.where("#{quoted_column} != ? OR #{quoted_column} IS NULL", value)
|
24
24
|
else
|
25
|
-
|
25
|
+
model.unscoped.where.not(updates)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -25,14 +25,10 @@ module OnlineMigrations
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def relation
|
28
|
-
|
28
|
+
model
|
29
29
|
.unscoped
|
30
|
-
.where(copy_to.
|
31
|
-
|
32
|
-
Utils.ar_where_not_multiple_conditions(
|
33
|
-
relation,
|
34
|
-
copy_from.map { |from_column| [from_column, nil] }.to_h
|
35
|
-
)
|
30
|
+
.where(copy_to.index_with(nil))
|
31
|
+
.where.not(copy_from.index_with(nil))
|
36
32
|
end
|
37
33
|
|
38
34
|
def process_batch(relation)
|
@@ -42,22 +38,18 @@ module OnlineMigrations
|
|
42
38
|
old_values = copy_from.map do |from_column|
|
43
39
|
old_value = arel_table[from_column]
|
44
40
|
if (type_cast_function = type_cast_functions[from_column])
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
old_value =
|
42
|
+
if type_cast_function.match?(/\A\w+\z/)
|
43
|
+
Arel::Nodes::NamedFunction.new(type_cast_function, [old_value])
|
44
|
+
else
|
45
|
+
# We got a cast expression.
|
46
|
+
Arel.sql(type_cast_function)
|
47
|
+
end
|
51
48
|
end
|
52
49
|
old_value
|
53
50
|
end
|
54
51
|
|
55
|
-
|
56
|
-
stmt = Arel::UpdateManager.new(arel.engine)
|
57
|
-
else
|
58
|
-
stmt = Arel::UpdateManager.new
|
59
|
-
end
|
60
|
-
|
52
|
+
stmt = Arel::UpdateManager.new
|
61
53
|
stmt.table(arel_table)
|
62
54
|
stmt.wheres = arel.constraints
|
63
55
|
|
@@ -12,29 +12,11 @@ module OnlineMigrations
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def relation
|
15
|
-
|
16
|
-
# https://github.com/rails/rails/pull/34727
|
17
|
-
associations.inject(model.unscoped) do |relation, association|
|
18
|
-
reflection = model.reflect_on_association(association)
|
19
|
-
if reflection.nil?
|
20
|
-
raise ArgumentError, "'#{model.name}' has no association called '#{association}'"
|
21
|
-
end
|
22
|
-
|
23
|
-
# left_joins was added in Active Record 5.0 - https://github.com/rails/rails/pull/12071
|
24
|
-
relation
|
25
|
-
.left_joins(association)
|
26
|
-
.where(reflection.table_name => { reflection.association_primary_key => nil })
|
27
|
-
end
|
15
|
+
model.unscoped.where.missing(*associations)
|
28
16
|
end
|
29
17
|
|
30
18
|
def process_batch(relation)
|
31
|
-
|
32
|
-
relation.delete_all
|
33
|
-
else
|
34
|
-
# Older Active Record generates incorrect query when running delete_all
|
35
|
-
primary_key = model.primary_key
|
36
|
-
model.unscoped.where(primary_key => relation.select(primary_key)).delete_all
|
37
|
-
end
|
19
|
+
relation.delete_all
|
38
20
|
end
|
39
21
|
|
40
22
|
def count
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module OnlineMigrations
|
4
4
|
module BackgroundMigrations
|
5
|
-
class Migration <
|
5
|
+
class Migration < ApplicationRecord
|
6
6
|
STATUSES = [
|
7
7
|
:enqueued, # The migration has been enqueued by the user.
|
8
8
|
:running, # The migration is being performed by a migration executor.
|
@@ -15,25 +15,29 @@ module OnlineMigrations
|
|
15
15
|
self.table_name = :background_migrations
|
16
16
|
|
17
17
|
scope :queue_order, -> { order(created_at: :asc) }
|
18
|
+
scope :runnable, -> { where(composite: false) }
|
18
19
|
scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
|
20
|
+
scope :except_succeeded, -> { where.not(status: :succeeded) }
|
19
21
|
scope :for_migration_name, ->(migration_name) { where(migration_name: normalize_migration_name(migration_name)) }
|
20
22
|
scope :for_configuration, ->(migration_name, arguments) do
|
21
23
|
for_migration_name(migration_name).where("arguments = ?", arguments.to_json)
|
22
24
|
end
|
23
25
|
|
24
|
-
enum status: STATUSES.
|
26
|
+
enum status: STATUSES.index_with(&:to_s)
|
25
27
|
|
28
|
+
belongs_to :parent, class_name: name, optional: true
|
29
|
+
has_many :children, class_name: name, foreign_key: :parent_id
|
26
30
|
has_many :migration_jobs
|
27
31
|
|
28
32
|
validates :migration_name, :batch_column_name, presence: true
|
29
33
|
|
30
|
-
validates :
|
31
|
-
|
34
|
+
validates :batch_size, :sub_batch_size, presence: true, numericality: { greater_than: 0 }
|
35
|
+
validates :min_value, :max_value, presence: true, numericality: { greater_than: 0, unless: :composite? }
|
32
36
|
|
33
37
|
validates :batch_pause, :sub_batch_pause_ms, presence: true,
|
34
38
|
numericality: { greater_than_or_equal_to: 0 }
|
35
|
-
validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
36
|
-
validates :arguments, uniqueness: { scope: :migration_name }
|
39
|
+
validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true, unless: :composite?
|
40
|
+
validates :arguments, uniqueness: { scope: [:migration_name, :shard] }
|
37
41
|
|
38
42
|
validate :validate_batch_column_values
|
39
43
|
validate :validate_batch_sizes
|
@@ -43,6 +47,8 @@ module OnlineMigrations
|
|
43
47
|
validates_with MigrationStatusValidator, on: :update
|
44
48
|
|
45
49
|
before_validation :set_defaults
|
50
|
+
before_create :create_child_migrations, if: :composite?
|
51
|
+
before_update :copy_attributes_to_children, if: :composite?
|
46
52
|
|
47
53
|
# @private
|
48
54
|
def self.normalize_migration_name(migration_name)
|
@@ -58,28 +64,53 @@ module OnlineMigrations
|
|
58
64
|
succeeded? || failed?
|
59
65
|
end
|
60
66
|
|
67
|
+
# Overwrite enum's generated method to correctly work for composite migrations.
|
68
|
+
def paused!
|
69
|
+
return super if !composite?
|
70
|
+
|
71
|
+
transaction do
|
72
|
+
super
|
73
|
+
children.each { |child| child.paused! if child.enqueued? || child.running? }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Overwrite enum's generated method to correctly work for composite migrations.
|
78
|
+
def running!
|
79
|
+
return super if !composite?
|
80
|
+
|
81
|
+
transaction do
|
82
|
+
super
|
83
|
+
children.each { |child| child.running! if child.paused? }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
61
87
|
def last_job
|
62
|
-
migration_jobs.order(max_value
|
88
|
+
migration_jobs.order(:max_value).last
|
63
89
|
end
|
64
90
|
|
65
91
|
def last_completed_job
|
66
|
-
migration_jobs.completed.order(finished_at
|
92
|
+
migration_jobs.completed.order(:finished_at).last
|
67
93
|
end
|
68
94
|
|
69
95
|
# Returns the progress of the background migration.
|
70
96
|
#
|
71
97
|
# @return [Float, nil]
|
72
|
-
# - when background migration is configured to not
|
73
|
-
# - otherwise returns value in range
|
98
|
+
# - when background migration is configured to not track progress, returns `nil`
|
99
|
+
# - otherwise returns value in range from 0.0 to 100.0
|
74
100
|
#
|
75
101
|
def progress
|
76
102
|
if succeeded?
|
77
|
-
|
103
|
+
100.0
|
104
|
+
elsif composite?
|
105
|
+
progresses = children.map(&:progress).compact
|
106
|
+
if progresses.any?
|
107
|
+
(progresses.sum / progresses.size).round(2)
|
108
|
+
end
|
78
109
|
elsif rows_count
|
79
110
|
jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
|
80
111
|
# The last migration job may need to process the amount of rows
|
81
112
|
# less than the batch size, so we can get a value > 1.0.
|
82
|
-
[jobs_rows_count.to_f / rows_count, 1.0].min
|
113
|
+
([jobs_rows_count.to_f / rows_count, 1.0].min * 100).round(2)
|
83
114
|
end
|
84
115
|
end
|
85
116
|
|
@@ -95,13 +126,19 @@ module OnlineMigrations
|
|
95
126
|
migration_object.relation
|
96
127
|
end
|
97
128
|
|
129
|
+
def migration_model
|
130
|
+
migration_relation.model
|
131
|
+
end
|
132
|
+
|
98
133
|
# Returns whether the interval between previous step run has passed.
|
99
134
|
# @return [Boolean]
|
100
135
|
#
|
101
136
|
def interval_elapsed?
|
102
|
-
|
137
|
+
last_active_job = migration_jobs.active.order(:updated_at).last
|
138
|
+
|
139
|
+
if last_active_job && !last_active_job.stuck?
|
103
140
|
false
|
104
|
-
elsif (job = last_completed_job)
|
141
|
+
elsif batch_pause > 0 && (job = last_completed_job)
|
105
142
|
job.finished_at + batch_pause <= Time.current
|
106
143
|
else
|
107
144
|
true
|
@@ -123,6 +160,14 @@ module OnlineMigrations
|
|
123
160
|
end
|
124
161
|
end
|
125
162
|
|
163
|
+
# @private
|
164
|
+
def on_shard(&block)
|
165
|
+
abstract_class = find_abstract_class(migration_model)
|
166
|
+
|
167
|
+
shard = (self.shard || abstract_class.default_shard).to_sym
|
168
|
+
abstract_class.connected_to(shard: shard, role: :writing, &block)
|
169
|
+
end
|
170
|
+
|
126
171
|
# @private
|
127
172
|
def reset_failed_jobs_attempts
|
128
173
|
iterator = BatchIterator.new(migration_jobs.failed.attempts_exceeded)
|
@@ -138,16 +183,10 @@ module OnlineMigrations
|
|
138
183
|
|
139
184
|
# rubocop:disable Lint/UnreachableLoop
|
140
185
|
iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
batch_range = relation.pluck("MIN(#{quoted_column}), MAX(#{quoted_column})").first
|
145
|
-
else
|
146
|
-
min = relation.arel_table[batch_column_name].minimum
|
147
|
-
max = relation.arel_table[batch_column_name].maximum
|
186
|
+
min = relation.arel_table[batch_column_name].minimum
|
187
|
+
max = relation.arel_table[batch_column_name].maximum
|
188
|
+
batch_range = relation.pick(min, max)
|
148
189
|
|
149
|
-
batch_range = relation.pluck(min, max).first
|
150
|
-
end
|
151
190
|
break
|
152
191
|
end
|
153
192
|
# rubocop:enable Lint/UnreachableLoop
|
@@ -162,6 +201,10 @@ module OnlineMigrations
|
|
162
201
|
[min_value, max_value]
|
163
202
|
end
|
164
203
|
|
204
|
+
protected
|
205
|
+
attr_accessor :child
|
206
|
+
alias child? child
|
207
|
+
|
165
208
|
private
|
166
209
|
def validate_batch_column_values
|
167
210
|
if max_value.to_i < min_value.to_i
|
@@ -176,7 +219,13 @@ module OnlineMigrations
|
|
176
219
|
end
|
177
220
|
|
178
221
|
def validate_jobs_status
|
179
|
-
if
|
222
|
+
if composite?
|
223
|
+
if succeeded? && children.except_succeeded.exists?
|
224
|
+
errors.add(:base, "all child migrations must be succeeded")
|
225
|
+
elsif failed? && !children.failed.exists?
|
226
|
+
errors.add(:base, "at least one child migration must be failed")
|
227
|
+
end
|
228
|
+
elsif succeeded? && migration_jobs.except_succeeded.exists?
|
180
229
|
errors.add(:base, "all migration jobs must be succeeded")
|
181
230
|
elsif failed? && !migration_jobs.failed.exists?
|
182
231
|
errors.add(:base, "at least one migration job must be failed")
|
@@ -185,12 +234,30 @@ module OnlineMigrations
|
|
185
234
|
|
186
235
|
def set_defaults
|
187
236
|
if migration_relation.is_a?(ActiveRecord::Relation)
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
self.
|
237
|
+
if !child?
|
238
|
+
shards = Utils.shard_names(migration_model)
|
239
|
+
self.composite = shards.size > 1
|
240
|
+
end
|
241
|
+
|
242
|
+
self.batch_column_name ||= migration_relation.primary_key
|
243
|
+
|
244
|
+
if composite?
|
245
|
+
self.min_value = self.max_value = self.rows_count = -1 # not relevant
|
246
|
+
else
|
247
|
+
on_shard do
|
248
|
+
self.min_value ||= migration_relation.minimum(batch_column_name)
|
249
|
+
self.max_value ||= migration_relation.maximum(batch_column_name)
|
250
|
+
|
251
|
+
# This can be the case when run in development on empty tables
|
252
|
+
if min_value.nil?
|
253
|
+
# integer IDs minimum value is 1
|
254
|
+
self.min_value = self.max_value = 1
|
255
|
+
end
|
256
|
+
|
257
|
+
count = migration_object.count
|
258
|
+
self.rows_count = count if count != :no_count
|
259
|
+
end
|
260
|
+
end
|
194
261
|
end
|
195
262
|
|
196
263
|
config = ::OnlineMigrations.config.background_migrations
|
@@ -199,12 +266,27 @@ module OnlineMigrations
|
|
199
266
|
self.batch_pause ||= config.batch_pause
|
200
267
|
self.sub_batch_pause_ms ||= config.sub_batch_pause_ms
|
201
268
|
self.batch_max_attempts ||= config.batch_max_attempts
|
269
|
+
end
|
270
|
+
|
271
|
+
def create_child_migrations
|
272
|
+
shards = Utils.shard_names(migration_model)
|
202
273
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
274
|
+
children = shards.map do |shard|
|
275
|
+
child = Migration.new(migration_name: migration_name, arguments: arguments, shard: shard)
|
276
|
+
child.child = true
|
277
|
+
child
|
207
278
|
end
|
279
|
+
|
280
|
+
self.children = children
|
281
|
+
end
|
282
|
+
|
283
|
+
def copy_attributes_to_children
|
284
|
+
attributes = [:batch_size, :sub_batch_size, :batch_pause, :sub_batch_pause_ms, :batch_max_attempts]
|
285
|
+
updates = {}
|
286
|
+
attributes.each do |attribute|
|
287
|
+
updates[attribute] = read_attribute(attribute) if attribute_changed?(attribute)
|
288
|
+
end
|
289
|
+
children.update_all(updates) if updates.any?
|
208
290
|
end
|
209
291
|
|
210
292
|
def next_min_value
|
@@ -214,6 +296,13 @@ module OnlineMigrations
|
|
214
296
|
min_value
|
215
297
|
end
|
216
298
|
end
|
299
|
+
|
300
|
+
def find_abstract_class(model)
|
301
|
+
model.ancestors.find do |parent|
|
302
|
+
parent == ActiveRecord::Base ||
|
303
|
+
(parent.is_a?(Class) && parent.abstract_class?)
|
304
|
+
end
|
305
|
+
end
|
217
306
|
end
|
218
307
|
end
|
219
308
|
end
|
@@ -223,10 +223,6 @@ module OnlineMigrations
|
|
223
223
|
# For smaller tables it is probably better and easier to directly find and delete orpahed records.
|
224
224
|
#
|
225
225
|
def delete_orphaned_records_in_background(model_name, *associations, **options)
|
226
|
-
if Utils.ar_version <= 4.2
|
227
|
-
raise "#{__method__} does not support Active Record <= 4.2 yet"
|
228
|
-
end
|
229
|
-
|
230
226
|
model_name = model_name.name if model_name.is_a?(Class)
|
231
227
|
|
232
228
|
enqueue_background_migration(
|