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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +112 -0
  3. data/.gitignore +10 -0
  4. data/.rubocop.yml +113 -0
  5. data/.yardopts +1 -0
  6. data/BACKGROUND_MIGRATIONS.md +288 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +27 -0
  9. data/Gemfile.lock +108 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +1067 -0
  12. data/Rakefile +23 -0
  13. data/gemfiles/activerecord_42.gemfile +6 -0
  14. data/gemfiles/activerecord_50.gemfile +5 -0
  15. data/gemfiles/activerecord_51.gemfile +5 -0
  16. data/gemfiles/activerecord_52.gemfile +5 -0
  17. data/gemfiles/activerecord_60.gemfile +5 -0
  18. data/gemfiles/activerecord_61.gemfile +5 -0
  19. data/gemfiles/activerecord_70.gemfile +5 -0
  20. data/gemfiles/activerecord_head.gemfile +5 -0
  21. data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
  22. data/lib/generators/online_migrations/install_generator.rb +34 -0
  23. data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
  24. data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
  25. data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
  26. data/lib/online_migrations/background_migration.rb +64 -0
  27. data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
  28. data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
  29. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
  30. data/lib/online_migrations/background_migrations/config.rb +98 -0
  31. data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
  32. data/lib/online_migrations/background_migrations/migration.rb +210 -0
  33. data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
  34. data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
  35. data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
  36. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
  37. data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
  38. data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
  39. data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
  40. data/lib/online_migrations/batch_iterator.rb +87 -0
  41. data/lib/online_migrations/change_column_type_helpers.rb +587 -0
  42. data/lib/online_migrations/command_checker.rb +590 -0
  43. data/lib/online_migrations/command_recorder.rb +137 -0
  44. data/lib/online_migrations/config.rb +198 -0
  45. data/lib/online_migrations/copy_trigger.rb +91 -0
  46. data/lib/online_migrations/database_tasks.rb +19 -0
  47. data/lib/online_migrations/error_messages.rb +388 -0
  48. data/lib/online_migrations/foreign_key_definition.rb +17 -0
  49. data/lib/online_migrations/foreign_keys_collector.rb +33 -0
  50. data/lib/online_migrations/indexes_collector.rb +48 -0
  51. data/lib/online_migrations/lock_retrier.rb +250 -0
  52. data/lib/online_migrations/migration.rb +63 -0
  53. data/lib/online_migrations/migrator.rb +23 -0
  54. data/lib/online_migrations/schema_cache.rb +96 -0
  55. data/lib/online_migrations/schema_statements.rb +1042 -0
  56. data/lib/online_migrations/utils.rb +140 -0
  57. data/lib/online_migrations/version.rb +5 -0
  58. data/lib/online_migrations.rb +74 -0
  59. data/online_migrations.gemspec +28 -0
  60. 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 4.2.0"
4
+ @pg_gem_requirement = "< 1"
5
+
6
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 5.0.0"
4
+
5
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 5.1.0"
4
+
5
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 5.2.0"
4
+
5
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 6.0.0"
4
+
5
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 6.1.0"
4
+
5
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = "~> 7.0.0"
4
+
5
+ eval_gemfile "../Gemfile"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ @ar_gem_requirement = { github: "rails/rails", branch: "main" }
4
+
5
+ eval_gemfile "../Gemfile"
@@ -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