online_migrations 0.1.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 +7 -0
- data/.github/workflows/test.yml +112 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +113 -0
- data/.yardopts +1 -0
- data/BACKGROUND_MIGRATIONS.md +288 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +1067 -0
- data/Rakefile +23 -0
- data/gemfiles/activerecord_42.gemfile +6 -0
- data/gemfiles/activerecord_50.gemfile +5 -0
- data/gemfiles/activerecord_51.gemfile +5 -0
- data/gemfiles/activerecord_52.gemfile +5 -0
- data/gemfiles/activerecord_60.gemfile +5 -0
- data/gemfiles/activerecord_61.gemfile +5 -0
- data/gemfiles/activerecord_70.gemfile +5 -0
- data/gemfiles/activerecord_head.gemfile +5 -0
- data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
- data/lib/generators/online_migrations/install_generator.rb +34 -0
- data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
- data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
- data/lib/online_migrations/background_migration.rb +64 -0
- data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
- data/lib/online_migrations/background_migrations/config.rb +98 -0
- data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
- data/lib/online_migrations/background_migrations/migration.rb +210 -0
- data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
- data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
- data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
- data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
- data/lib/online_migrations/batch_iterator.rb +87 -0
- data/lib/online_migrations/change_column_type_helpers.rb +587 -0
- data/lib/online_migrations/command_checker.rb +590 -0
- data/lib/online_migrations/command_recorder.rb +137 -0
- data/lib/online_migrations/config.rb +198 -0
- data/lib/online_migrations/copy_trigger.rb +91 -0
- data/lib/online_migrations/database_tasks.rb +19 -0
- data/lib/online_migrations/error_messages.rb +388 -0
- data/lib/online_migrations/foreign_key_definition.rb +17 -0
- data/lib/online_migrations/foreign_keys_collector.rb +33 -0
- data/lib/online_migrations/indexes_collector.rb +48 -0
- data/lib/online_migrations/lock_retrier.rb +250 -0
- data/lib/online_migrations/migration.rb +63 -0
- data/lib/online_migrations/migrator.rb +23 -0
- data/lib/online_migrations/schema_cache.rb +96 -0
- data/lib/online_migrations/schema_statements.rb +1042 -0
- data/lib/online_migrations/utils.rb +140 -0
- data/lib/online_migrations/version.rb +5 -0
- data/lib/online_migrations.rb +74 -0
- data/online_migrations.gemspec +28 -0
- metadata +119 -0
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "rdoc/task"
|
13
|
+
|
14
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
15
|
+
rdoc.rdoc_dir = "rdoc"
|
16
|
+
rdoc.title = "OnlineMigrations"
|
17
|
+
rdoc.options << "--line-numbers"
|
18
|
+
rdoc.rdoc_files.include("README.md")
|
19
|
+
rdoc.rdoc_files.include("BACKGROUND_MIGRATIONS.md")
|
20
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
21
|
+
end
|
22
|
+
|
23
|
+
task default: :test
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module OnlineMigrations
|
6
|
+
# @private
|
7
|
+
class BackgroundMigrationGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
desc "This generator creates a background migration file."
|
10
|
+
|
11
|
+
def create_background_migration_file
|
12
|
+
template_file = File.join(
|
13
|
+
"lib/#{migrations_module_file_path}",
|
14
|
+
class_path,
|
15
|
+
"#{file_name}.rb"
|
16
|
+
)
|
17
|
+
template("background_migration.rb", template_file)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def migrations_module_file_path
|
22
|
+
migrations_module.underscore
|
23
|
+
end
|
24
|
+
|
25
|
+
def migrations_module
|
26
|
+
OnlineMigrations.config.background_migrations.migrations_module
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
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 InstallGenerator < Rails::Generators::Base
|
9
|
+
include ActiveRecord::Generators::Migration
|
10
|
+
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
12
|
+
|
13
|
+
def create_migration_file
|
14
|
+
migration_template("migration.rb", File.join(migrations_dir, "install_online_migrations.rb"))
|
15
|
+
end
|
16
|
+
|
17
|
+
def copy_initializer_file
|
18
|
+
template("initializer.rb", "config/initializers/online_migrations.rb")
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def migration_parent
|
23
|
+
Utils.migration_parent_string
|
24
|
+
end
|
25
|
+
|
26
|
+
def start_after
|
27
|
+
self.class.next_migration_number(migrations_dir)
|
28
|
+
end
|
29
|
+
|
30
|
+
def migrations_dir
|
31
|
+
Utils.ar_version >= 5.1 ? db_migrate_path : "db/migrate"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module <%= migrations_module %>
|
4
|
+
<% module_namespacing do -%>
|
5
|
+
class <%= class_name %> < OnlineMigrations::BackgroundMigration
|
6
|
+
def relation
|
7
|
+
# ActiveRecord::Relation to be iterated over
|
8
|
+
end
|
9
|
+
|
10
|
+
def process_batch(relation)
|
11
|
+
# The work to be done in a single iteration of the background migration.
|
12
|
+
# This should be idempotent, as the same batch may be processed more
|
13
|
+
# than once if the background migration is interrupted and resumed.
|
14
|
+
end
|
15
|
+
|
16
|
+
def count
|
17
|
+
# Optionally, define the number of rows that will be iterated over.
|
18
|
+
# This is used to track the background migration's progress.
|
19
|
+
end
|
20
|
+
end
|
21
|
+
<% end -%>
|
22
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
OnlineMigrations.configure do |config|
|
4
|
+
# Configure the migration version starting after which checks are performed.
|
5
|
+
# config.start_after = <%= start_after %>
|
6
|
+
|
7
|
+
# Set the version of the production database so the right checks are run in development.
|
8
|
+
# config.target_version = 10
|
9
|
+
|
10
|
+
# Configure whether to perform checks when migrating down.
|
11
|
+
config.check_down = false
|
12
|
+
|
13
|
+
# Configure custom error messages.
|
14
|
+
# error_messages is a Hash with keys - error names and values - error messages.
|
15
|
+
# config.error_messages[:remove_column] = "Your custom instructions"
|
16
|
+
|
17
|
+
# Maximum allowed lock timeout value (in seconds).
|
18
|
+
# If set lock timeout is greater than this value, the migration will fail.
|
19
|
+
# config.lock_timeout_limit = 10.seconds
|
20
|
+
|
21
|
+
# Configure list of tables with permanently small number of records.
|
22
|
+
# This tables are usually tables like "settings", "prices", "plans" etc.
|
23
|
+
# It is considered safe to perform most of the dangerous operations on them.
|
24
|
+
# config.small_tables = []
|
25
|
+
|
26
|
+
# Disable specific checks.
|
27
|
+
# For the list of available checks look at `lib/error_messages` folder.
|
28
|
+
# config.disable_check(:remove_index)
|
29
|
+
|
30
|
+
# Enable specific checks. All checks are enabled by default,
|
31
|
+
# but this may change in the future.
|
32
|
+
# For the list of available checks look at `lib/error_messages` folder.
|
33
|
+
# config.enable_check(:remove_index)
|
34
|
+
|
35
|
+
# Lock retries.
|
36
|
+
# Configure your custom lock retrier (see LockRetrier).
|
37
|
+
# To disable lock retries, set `lock_retrier` to `nil`.
|
38
|
+
config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
|
39
|
+
attempts: 30, # attempt 30 retries
|
40
|
+
base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
|
41
|
+
max_delay: 1.minute, # up to the maximum delay of 1 minute
|
42
|
+
lock_timeout: 0.05.seconds # and 50ms set as lock timeout for each try
|
43
|
+
)
|
44
|
+
|
45
|
+
# Configure tables that are in the process of being renamed.
|
46
|
+
# config.table_renames["users"] = "clients"
|
47
|
+
|
48
|
+
# Configure columns that are in the process of being renamed.
|
49
|
+
# config.column_renames["users] = { "name" => "first_name" }
|
50
|
+
|
51
|
+
# Add custom checks. Use the `stop!` method to stop migrations.
|
52
|
+
#
|
53
|
+
# config.add_check do |method, args|
|
54
|
+
# if method == :add_column && args[0].to_s == "users"
|
55
|
+
# stop!("No more columns on the users table")
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
|
59
|
+
# ==> Background migrations configuration
|
60
|
+
# The number of rows to process in a single background migration run.
|
61
|
+
# config.backround_migrations.batch_size = 20_000
|
62
|
+
|
63
|
+
# The smaller batches size that the batches will be divided into.
|
64
|
+
# config.backround_migrations.sub_batch_size = 1000
|
65
|
+
|
66
|
+
# The pause interval between each background migration job's execution (in seconds).
|
67
|
+
# config.backround_migrations.batch_pause = 0.seconds
|
68
|
+
|
69
|
+
# The number of milliseconds to sleep between each sub_batch execution.
|
70
|
+
# config.backround_migrations.sub_batch_pause_ms = 100
|
71
|
+
|
72
|
+
# Maximum number of batch run attempts.
|
73
|
+
# When attempts are exhausted, the individual batch is marked as failed.
|
74
|
+
# config.backround_migrations.batch_max_attempts = 5
|
75
|
+
|
76
|
+
# Configure custom throttler for background migrations.
|
77
|
+
# It will be called before each batch run.
|
78
|
+
# If throttled, the current run will be retried next time.
|
79
|
+
# config.backround_migrations.throttler = -> { DatabaseStatus.unhealthy? }
|
80
|
+
|
81
|
+
# The number of seconds that must pass before the running job is considered stuck.
|
82
|
+
# config.background_migrations.stuck_jobs_timeout = 1.hour
|
83
|
+
|
84
|
+
# The Active Support backtrace cleaner that will be used to clean the
|
85
|
+
# backtrace of a migration job that errors.
|
86
|
+
config.background_migrations.backtrace_cleaner = Rails.backtrace_cleaner
|
87
|
+
|
88
|
+
# The callback to perform when an error occurs in the migration job.
|
89
|
+
# config.backround_migrations.error_handler = ->(error, errored_job) do
|
90
|
+
# Bugsnag.notify(error) do |notification|
|
91
|
+
# notification.add_metadata(:background_migration, { name: errored_job.migration_name })
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class InstallOnlineMigrations < <%= migration_parent %>
|
2
|
+
def change
|
3
|
+
create_table :background_migrations do |t|
|
4
|
+
t.string :migration_name, null: false
|
5
|
+
t.jsonb :arguments, default: [], null: false
|
6
|
+
t.string :batch_column_name, null: false
|
7
|
+
t.bigint :min_value, null: false
|
8
|
+
t.bigint :max_value, null: false
|
9
|
+
t.bigint :rows_count
|
10
|
+
t.integer :batch_size, null: false
|
11
|
+
t.integer :sub_batch_size, null: false
|
12
|
+
t.integer :batch_pause, null: false
|
13
|
+
t.integer :sub_batch_pause_ms, null: false
|
14
|
+
t.integer :batch_max_attempts, null: false
|
15
|
+
t.string :status, default: "enqueued", null: false
|
16
|
+
t.timestamps null: false
|
17
|
+
|
18
|
+
t.index [:migration_name, :arguments],
|
19
|
+
unique: true, name: :index_background_migrations_on_unique_configuration
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :background_migration_jobs do |t|
|
23
|
+
t.bigint :migration_id, null: false
|
24
|
+
t.bigint :min_value, null: false
|
25
|
+
t.bigint :max_value, null: false
|
26
|
+
t.integer :batch_size, null: false
|
27
|
+
t.integer :sub_batch_size, null: false
|
28
|
+
t.integer :pause_ms, null: false
|
29
|
+
t.datetime :started_at
|
30
|
+
t.datetime :finished_at
|
31
|
+
t.string :status, default: "enqueued", null: false
|
32
|
+
t.integer :max_attempts, null: false
|
33
|
+
t.integer :attempts, default: 0, null: false
|
34
|
+
t.string :error_class
|
35
|
+
t.string :error_message
|
36
|
+
t.string :backtrace, array: true
|
37
|
+
t.timestamps null: false
|
38
|
+
|
39
|
+
t.foreign_key :background_migrations, column: :migration_id, on_delete: :cascade
|
40
|
+
|
41
|
+
t.index [:migration_id, :max_value], name: :index_background_migration_jobs_on_max_value
|
42
|
+
t.index [:migration_id, :status, :updated_at], name: :index_background_migration_jobs_on_updated_at
|
43
|
+
t.index [:migration_id, :finished_at], name: :index_background_migration_jobs_on_finished_at
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
# Base class that is inherited by the host application's background migration classes.
|
5
|
+
class BackgroundMigration
|
6
|
+
class NotFoundError < NameError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Finds a Background Migration with the given name.
|
10
|
+
#
|
11
|
+
# @param name [String] the name of the Background Migration to be found.
|
12
|
+
#
|
13
|
+
# @return [BackgroundMigration] the Background Migration with the given name.
|
14
|
+
#
|
15
|
+
# @raise [NotFoundError] if a Background Migration with the given name does not exist.
|
16
|
+
#
|
17
|
+
def named(name)
|
18
|
+
namespace = OnlineMigrations.config.background_migrations.migrations_module.constantize
|
19
|
+
internal_namespace = ::OnlineMigrations::BackgroundMigrations
|
20
|
+
|
21
|
+
migration = "#{namespace}::#{name}".safe_constantize ||
|
22
|
+
"#{internal_namespace}::#{name}".safe_constantize
|
23
|
+
|
24
|
+
raise NotFoundError.new("Background Migration #{name} not found", name) unless migration
|
25
|
+
unless migration.is_a?(Class) && migration < self
|
26
|
+
raise NotFoundError.new("#{name} is not a Background Migration", name)
|
27
|
+
end
|
28
|
+
|
29
|
+
migration
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The relation to be iterated over.
|
34
|
+
#
|
35
|
+
# @return [ActiveRecord::Relation]
|
36
|
+
#
|
37
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
38
|
+
# implement an override for this method.
|
39
|
+
#
|
40
|
+
def relation
|
41
|
+
raise NotImplementedError, "#{self.class.name} must implement a 'relation' method"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Processes one batch.
|
45
|
+
#
|
46
|
+
# @param _relation [ActiveRecord::Relation] the current batch from the enumerator being iterated
|
47
|
+
# @return [void]
|
48
|
+
#
|
49
|
+
# @raise [NotImplementedError] with a message advising subclasses to
|
50
|
+
# implement an override for this method.
|
51
|
+
#
|
52
|
+
def process_batch(_relation)
|
53
|
+
raise NotImplementedError, "#{self.class.name} must implement a 'process_batch' method"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the count of rows that will be iterated over (optional, to be able to show progress).
|
57
|
+
#
|
58
|
+
# @return [Integer, nil, :no_count]
|
59
|
+
#
|
60
|
+
def count
|
61
|
+
:no_count
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
|
5
|
+
module OnlineMigrations
|
6
|
+
module BackgroundMigrations
|
7
|
+
# @private
|
8
|
+
class AdvisoryLock
|
9
|
+
attr_reader :name, :connection
|
10
|
+
|
11
|
+
def initialize(name:, connection: ActiveRecord::Base.connection)
|
12
|
+
@name = name
|
13
|
+
@connection = connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def try_lock
|
17
|
+
locked = connection.select_value("SELECT pg_try_advisory_lock(#{lock_key})")
|
18
|
+
Utils.to_bool(locked)
|
19
|
+
end
|
20
|
+
|
21
|
+
def unlock
|
22
|
+
connection.select_value("SELECT pg_advisory_unlock(#{lock_key})")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Runs the given block if an advisory lock is able to be acquired.
|
26
|
+
def with_lock
|
27
|
+
if try_lock
|
28
|
+
begin
|
29
|
+
yield
|
30
|
+
ensure
|
31
|
+
unlock
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def active?
|
37
|
+
objid = lock_key & 0xffffffff
|
38
|
+
classid = (lock_key & (0xffffffff << 32)) >> 32
|
39
|
+
|
40
|
+
active = connection.select_value(<<~SQL)
|
41
|
+
SELECT granted
|
42
|
+
FROM pg_locks
|
43
|
+
WHERE locktype = 'advisory'
|
44
|
+
AND pid = pg_backend_pid()
|
45
|
+
AND mode = 'ExclusiveLock'
|
46
|
+
AND classid = #{classid}
|
47
|
+
AND objid = #{objid}
|
48
|
+
SQL
|
49
|
+
|
50
|
+
Utils.to_bool(active)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
SALT = 936723412
|
55
|
+
|
56
|
+
def lock_key
|
57
|
+
name_hash = Zlib.crc32(name)
|
58
|
+
SALT * name_hash
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundMigrations
|
5
|
+
# @private
|
6
|
+
class BackfillColumn < BackgroundMigration
|
7
|
+
attr_reader :table_name, :updates, :model_name
|
8
|
+
|
9
|
+
def initialize(table_name, updates, model_name = nil)
|
10
|
+
@table_name = table_name
|
11
|
+
@updates = updates
|
12
|
+
@model_name = model_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def relation
|
16
|
+
if updates.size == 1 && (column, value = updates.first) && !value.nil?
|
17
|
+
# If value is nil, the generated SQL is correct (`WHERE column IS NOT NULL`).
|
18
|
+
# Otherwise, the SQL is `WHERE column != value`. This condition ignores column
|
19
|
+
# with NULLs in it, so we need to also manually check for NULLs.
|
20
|
+
quoted_column = connection.quote_column_name(column)
|
21
|
+
model.where("#{quoted_column} != ? OR #{quoted_column} IS NULL", value)
|
22
|
+
else
|
23
|
+
Utils.ar_where_not_multiple_conditions(model, updates)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_batch(relation)
|
28
|
+
relation.update_all(updates)
|
29
|
+
end
|
30
|
+
|
31
|
+
def count
|
32
|
+
# Exact counts are expensive on large tables, since PostgreSQL
|
33
|
+
# needs to do a full scan. An estimated count should give a pretty decent
|
34
|
+
# approximation of rows count in this case.
|
35
|
+
Utils.estimated_count(connection, table_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def model
|
40
|
+
@model ||= if model_name.present?
|
41
|
+
Object.const_get(model_name, false)
|
42
|
+
else
|
43
|
+
Utils.define_model(ActiveRecord::Base.connection, table_name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def connection
|
48
|
+
model.connection
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundMigrations
|
5
|
+
# @private
|
6
|
+
class BackgroundMigrationClassValidator < ActiveModel::Validator
|
7
|
+
def validate(record)
|
8
|
+
relation = record.migration_relation
|
9
|
+
migration_name = record.migration_name
|
10
|
+
|
11
|
+
unless relation.is_a?(ActiveRecord::Relation)
|
12
|
+
record.errors.add(
|
13
|
+
:migration_name,
|
14
|
+
"#{migration_name}#relation must return an ActiveRecord::Relation object"
|
15
|
+
)
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
if relation.joins_values.present? && !record.batch_column_name.to_s.include?(".")
|
20
|
+
record.errors.add(
|
21
|
+
:batch_column_name,
|
22
|
+
"must be a fully-qualified column if you join a table"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
if relation.arel.orders.present? || relation.arel.taken.present?
|
27
|
+
record.errors.add(
|
28
|
+
:migration_name,
|
29
|
+
"#{migration_name}#relation cannot use ORDER BY or LIMIT due to the way how iteration with a cursor is designed. " \
|
30
|
+
"You can use other ways to limit the number of rows, e.g. a WHERE condition on the primary key column."
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
module BackgroundMigrations
|
5
|
+
# Class representing configuration options for background migrations.
|
6
|
+
class Config
|
7
|
+
# The module to namespace background migrations in
|
8
|
+
# @return [String] defaults to "OnlineMigrations::BackgroundMigrations"
|
9
|
+
attr_accessor :migrations_module
|
10
|
+
|
11
|
+
# The number of rows to process in a single background migration run
|
12
|
+
# @return [Integer] defaults to 20_000
|
13
|
+
#
|
14
|
+
attr_accessor :batch_size
|
15
|
+
|
16
|
+
# The smaller batches size that the batches will be divided into
|
17
|
+
# @return [Integer] defaults to 1000
|
18
|
+
#
|
19
|
+
attr_accessor :sub_batch_size
|
20
|
+
|
21
|
+
# The pause interval between each background migration job's execution (in seconds)
|
22
|
+
# @return [Integer] defaults to 0
|
23
|
+
#
|
24
|
+
attr_accessor :batch_pause
|
25
|
+
|
26
|
+
# The number of milliseconds to sleep between each sub_batch execution
|
27
|
+
# @return [Integer] defaults to 100 milliseconds
|
28
|
+
#
|
29
|
+
attr_accessor :sub_batch_pause_ms
|
30
|
+
|
31
|
+
# Maximum number of batch run attempts
|
32
|
+
#
|
33
|
+
# When attempts are exhausted, the individual batch is marked as failed.
|
34
|
+
# @return [Integer] defaults to 5
|
35
|
+
#
|
36
|
+
attr_accessor :batch_max_attempts
|
37
|
+
|
38
|
+
# Allows to throttle background migrations based on external signal (e.g. database health)
|
39
|
+
#
|
40
|
+
# It will be called before each batch run.
|
41
|
+
# If throttled, the current run will be retried next time.
|
42
|
+
#
|
43
|
+
# @return [Proc]
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# OnlineMigrations.config.backround_migrations.throttler = -> { DatabaseStatus.unhealthy? }
|
47
|
+
#
|
48
|
+
attr_reader :throttler
|
49
|
+
|
50
|
+
# The number of seconds that must pass before the running job is considered stuck
|
51
|
+
#
|
52
|
+
# @return [Integer] defaults to 1 hour
|
53
|
+
#
|
54
|
+
attr_accessor :stuck_jobs_timeout
|
55
|
+
|
56
|
+
# The Active Support backtrace cleaner that will be used to clean the
|
57
|
+
# backtrace of a migration job that errors.
|
58
|
+
#
|
59
|
+
# @return [ActiveSupport::BacktraceCleaner, nil] the backtrace cleaner to
|
60
|
+
# use when cleaning a job's backtrace. Defaults to `Rails.backtrace_cleaner`
|
61
|
+
#
|
62
|
+
attr_accessor :backtrace_cleaner
|
63
|
+
|
64
|
+
# The callback to perform when an error occurs in the migration job.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# OnlineMigrations.config.backround_migrations.error_handler = ->(error, errored_job) do
|
68
|
+
# Bugsnag.notify(error) do |notification|
|
69
|
+
# notification.add_metadata(:background_migration, { name: errored_job.migration_name })
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @return [Proc] the callback to perform when an error occurs in the migration job
|
74
|
+
#
|
75
|
+
attr_accessor :error_handler
|
76
|
+
|
77
|
+
def initialize
|
78
|
+
@migrations_module = "OnlineMigrations::BackgroundMigrations"
|
79
|
+
@batch_size = 20_000
|
80
|
+
@sub_batch_size = 1000
|
81
|
+
@batch_pause = 0.seconds
|
82
|
+
@sub_batch_pause_ms = 100
|
83
|
+
@batch_max_attempts = 5
|
84
|
+
@throttler = -> { false }
|
85
|
+
@stuck_jobs_timeout = 1.hour
|
86
|
+
@error_handler = ->(error, errored_job) {}
|
87
|
+
end
|
88
|
+
|
89
|
+
def throttler=(value)
|
90
|
+
unless value.respond_to?(:call)
|
91
|
+
raise ArgumentError, "background_migrations throttler must be a callable."
|
92
|
+
end
|
93
|
+
|
94
|
+
@throttler = value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|