online_migrations 0.26.0 → 0.27.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/docs/0.27-upgrade.md +24 -0
  4. data/docs/background_data_migrations.md +200 -101
  5. data/docs/background_schema_migrations.md +2 -2
  6. data/lib/generators/online_migrations/{background_migration_generator.rb → data_migration_generator.rb} +4 -4
  7. data/lib/generators/online_migrations/templates/change_background_data_migrations.rb.tt +34 -0
  8. data/lib/generators/online_migrations/templates/{background_data_migration.rb.tt → data_migration.rb.tt} +8 -9
  9. data/lib/generators/online_migrations/templates/initializer.rb.tt +19 -25
  10. data/lib/generators/online_migrations/templates/install_migration.rb.tt +9 -40
  11. data/lib/generators/online_migrations/upgrade_generator.rb +16 -8
  12. data/lib/online_migrations/active_record_batch_enumerator.rb +8 -0
  13. data/lib/online_migrations/background_data_migrations/backfill_column.rb +50 -0
  14. data/lib/online_migrations/background_data_migrations/config.rb +62 -0
  15. data/lib/online_migrations/{background_migrations → background_data_migrations}/copy_column.rb +15 -28
  16. data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_associated_records.rb +9 -5
  17. data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_orphaned_records.rb +5 -9
  18. data/lib/online_migrations/background_data_migrations/migration.rb +312 -0
  19. data/lib/online_migrations/{background_migrations → background_data_migrations}/migration_helpers.rb +72 -61
  20. data/lib/online_migrations/background_data_migrations/migration_job.rb +158 -0
  21. data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +65 -0
  22. data/lib/online_migrations/{background_migrations → background_data_migrations}/perform_action_on_relation.rb +5 -5
  23. data/lib/online_migrations/{background_migrations → background_data_migrations}/reset_counters.rb +5 -5
  24. data/lib/online_migrations/background_data_migrations/scheduler.rb +78 -0
  25. data/lib/online_migrations/background_data_migrations/ticker.rb +62 -0
  26. data/lib/online_migrations/background_schema_migrations/config.rb +2 -2
  27. data/lib/online_migrations/background_schema_migrations/migration.rb +51 -123
  28. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +25 -46
  29. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +43 -97
  30. data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
  31. data/lib/online_migrations/change_column_type_helpers.rb +3 -2
  32. data/lib/online_migrations/config.rb +4 -4
  33. data/lib/online_migrations/data_migration.rb +127 -0
  34. data/lib/online_migrations/lock_retrier.rb +5 -2
  35. data/lib/online_migrations/schema_statements.rb +1 -1
  36. data/lib/online_migrations/shard_aware.rb +44 -0
  37. data/lib/online_migrations/version.rb +1 -1
  38. data/lib/online_migrations.rb +18 -11
  39. metadata +22 -21
  40. data/lib/online_migrations/background_migration.rb +0 -64
  41. data/lib/online_migrations/background_migrations/backfill_column.rb +0 -54
  42. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -29
  43. data/lib/online_migrations/background_migrations/config.rb +0 -74
  44. data/lib/online_migrations/background_migrations/migration.rb +0 -329
  45. data/lib/online_migrations/background_migrations/migration_job.rb +0 -109
  46. data/lib/online_migrations/background_migrations/migration_job_runner.rb +0 -66
  47. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +0 -29
  48. data/lib/online_migrations/background_migrations/migration_runner.rb +0 -161
  49. data/lib/online_migrations/background_migrations/migration_status_validator.rb +0 -48
  50. data/lib/online_migrations/background_migrations/scheduler.rb +0 -42
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundDataMigrations
5
+ # Sidekiq job responsible for running background data migrations.
6
+ class MigrationJob
7
+ include Sidekiq::IterableJob
8
+
9
+ sidekiq_retry_in do |count, _exception, jobhash|
10
+ migration_id = jobhash["args"].fetch(0)
11
+ migration = Migration.find(migration_id)
12
+
13
+ if count + 1 >= migration.max_attempts
14
+ :kill
15
+ end
16
+ end
17
+
18
+ sidekiq_retries_exhausted do |jobhash, exception|
19
+ migration_id = jobhash["args"].fetch(0)
20
+ migration = Migration.find(migration_id)
21
+ migration.persist_error(exception)
22
+
23
+ OnlineMigrations.config.background_data_migrations.error_handler.call(exception, migration)
24
+ end
25
+
26
+ TICKER_INTERVAL = 5 # seconds
27
+
28
+ def initialize
29
+ super
30
+
31
+ @migration = nil
32
+ @data_migration = nil
33
+
34
+ @ticker = Ticker.new(TICKER_INTERVAL) do |ticks, duration|
35
+ # TODO: use 'cursor' accessor from sidekiq in the future.
36
+ # https://github.com/sidekiq/sidekiq/pull/6606
37
+ @migration.persist_progress(@_cursor, ticks, duration)
38
+ @migration.reload
39
+ end
40
+
41
+ @throttle_checked_at = current_time
42
+ end
43
+
44
+ def on_start
45
+ @migration.start
46
+ end
47
+
48
+ def around_iteration(&block)
49
+ @migration.on_shard_if_present(&block)
50
+ end
51
+
52
+ def on_resume
53
+ @data_migration.after_resume
54
+ end
55
+
56
+ def on_stop
57
+ @ticker.persist
58
+ @migration.stop
59
+ end
60
+
61
+ def on_complete
62
+ # Job was manually cancelled.
63
+ @migration.cancel if cancelled?
64
+
65
+ @migration.complete
66
+ end
67
+
68
+ def build_enumerator(migration_id, cursor:)
69
+ @migration = BackgroundDataMigrations::Migration.find(migration_id)
70
+ cursor ||= @migration.cursor
71
+
72
+ @migration.on_shard_if_present do
73
+ @data_migration = @migration.data_migration
74
+ collection_enum = @data_migration.build_enumerator(cursor: cursor)
75
+
76
+ if collection_enum
77
+ if !collection_enum.is_a?(Enumerator)
78
+ raise ArgumentError, <<~MSG.squish
79
+ #{@data_migration.class.name}#build_enumerator must return an Enumerator,
80
+ got #{collection_enum.class.name}.
81
+ MSG
82
+ end
83
+
84
+ collection_enum
85
+ else
86
+ collection = @data_migration.collection
87
+
88
+ case collection
89
+ when ActiveRecord::Relation
90
+ options = {
91
+ cursor: cursor,
92
+ batch_size: @data_migration.class.active_record_enumerator_batch_size || 100,
93
+ }
94
+ active_record_records_enumerator(collection, **options)
95
+ when ActiveRecord::Batches::BatchEnumerator
96
+ if collection.start || collection.finish
97
+ raise ArgumentError, <<~MSG.squish
98
+ #{@data_migration.class.name}#collection does not support
99
+ a batch enumerator with the "start" or "finish" options.
100
+ MSG
101
+ end
102
+
103
+ active_record_relations_enumerator(
104
+ collection.relation,
105
+ batch_size: collection.batch_size,
106
+ cursor: cursor,
107
+ use_ranges: collection.use_ranges
108
+ )
109
+ when Array
110
+ array_enumerator(collection, cursor: cursor)
111
+ else
112
+ raise ArgumentError, <<~MSG.squish
113
+ #{@data_migration.class.name}#collection must be either an ActiveRecord::Relation,
114
+ ActiveRecord::Batches::BatchEnumerator, or Array.
115
+ MSG
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def each_iteration(item, _migration_id)
122
+ if @migration.cancelling? || @migration.pausing? || @migration.paused?
123
+ # Finish this exact sidekiq job. When the migration is paused
124
+ # and will be resumed, a new job will be enqueued.
125
+ finished = true
126
+ throw :abort, finished
127
+ elsif should_throttle?
128
+ ActiveSupport::Notifications.instrument("throttled.background_data_migrations", migration: @migration)
129
+ finished = false
130
+ throw :abort, finished
131
+ else
132
+ @data_migration.around_process do
133
+ @migration.data_migration.process(item)
134
+
135
+ pause = OnlineMigrations.config.background_data_migrations.iteration_pause
136
+ sleep(pause) if pause > 0
137
+ end
138
+ @ticker.tick
139
+ end
140
+ end
141
+
142
+ private
143
+ THROTTLE_CHECK_INTERVAL = 5 # seconds
144
+ private_constant :THROTTLE_CHECK_INTERVAL
145
+
146
+ def should_throttle?
147
+ if current_time - @throttle_checked_at >= THROTTLE_CHECK_INTERVAL
148
+ @throttle_checked_at = current_time
149
+ OnlineMigrations.config.throttler.call
150
+ end
151
+ end
152
+
153
+ def current_time
154
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundDataMigrations
5
+ # @private
6
+ class MigrationStatusValidator < ActiveModel::Validator
7
+ # Valid status transitions a Migration can make.
8
+ VALID_STATUS_TRANSITIONS = {
9
+ # enqueued -> running occurs when the migration starts performing.
10
+ # enqueued -> paused occurs when the migration is paused before starting.
11
+ # enqueued -> cancelled occurs when the migration is cancelled before starting.
12
+ # enqueued -> failed occurs when the migration job fails to be enqueued, or
13
+ # if the migration is deleted before is starts running.
14
+ "enqueued" => ["running", "paused", "cancelled", "failed"],
15
+ # running -> succeeded occurs when the migration completes successfully.
16
+ # running -> pausing occurs when a user pauses the migration as it's performing.
17
+ # running -> cancelling occurs when a user cancels the migration as it's performing.
18
+ # running -> failed occurs when the job raises an exception when running.
19
+ "running" => [
20
+ "succeeded",
21
+ "pausing",
22
+ "cancelling",
23
+ "failed",
24
+ ],
25
+ # pausing -> paused occurs when the migration actually halts performing and
26
+ # occupies a status of paused.
27
+ # pausing -> cancelling occurs when the user cancels a migration immediately
28
+ # after it was paused, such that the migration had not actually halted yet.
29
+ # pausing -> succeeded occurs when the migration completes immediately after
30
+ # being paused. This can happen if the migration is on its last iteration
31
+ # when it is paused, or if the migration is paused after enqueue but has
32
+ # nothing in its collection to process.
33
+ # pausing -> failed occurs when the job raises an exception after the
34
+ # user has paused it.
35
+ "pausing" => ["paused", "cancelling", "succeeded", "failed"],
36
+ # paused -> enqueued occurs when the migration is resumed after being paused.
37
+ # paused -> cancelled when the user cancels the migration after it is paused.
38
+ "paused" => ["enqueued", "cancelled"],
39
+ # failed -> enqueued occurs when the migration is retried after encounting an error.
40
+ "failed" => ["enqueued"],
41
+ # cancelling -> cancelled occurs when the migration actually halts performing
42
+ # and occupies a status of cancelled.
43
+ # cancelling -> succeeded occurs when the migration completes immediately after
44
+ # being cancelled. See description for pausing -> succeeded.
45
+ # cancelling -> failed occurs when the job raises an exception after the
46
+ # user has cancelled it.
47
+ "cancelling" => ["cancelled", "succeeded", "failed"],
48
+ }
49
+
50
+ def validate(record)
51
+ return if !record.status_changed?
52
+
53
+ previous_status, new_status = record.status_change
54
+ valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
55
+
56
+ if !valid_new_statuses.include?(new_status)
57
+ record.errors.add(
58
+ :status,
59
+ "cannot transition data migration from status '#{previous_status}' to '#{new_status}'"
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- module BackgroundMigrations
4
+ module BackgroundDataMigrations
5
5
  # @private
6
- class PerformActionOnRelation < BackgroundMigration
6
+ class PerformActionOnRelation < DataMigration
7
7
  attr_reader :model, :conditions, :action, :options
8
8
 
9
9
  def initialize(model_name, conditions, action, options = {})
@@ -13,11 +13,11 @@ module OnlineMigrations
13
13
  @options = options.symbolize_keys
14
14
  end
15
15
 
16
- def relation
17
- model.unscoped.where(conditions)
16
+ def collection
17
+ model.unscoped.where(conditions).in_batches(of: 100)
18
18
  end
19
19
 
20
- def process_batch(relation)
20
+ def process(relation)
21
21
  case action
22
22
  when :update_all
23
23
  updates = options.fetch(:updates)
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- module BackgroundMigrations
4
+ module BackgroundDataMigrations
5
5
  # @private
6
- class ResetCounters < BackgroundMigration
6
+ class ResetCounters < DataMigration
7
7
  attr_reader :model, :counters, :touch
8
8
 
9
9
  def initialize(model_name, counters, options = {})
@@ -12,11 +12,11 @@ module OnlineMigrations
12
12
  @touch = options[:touch]
13
13
  end
14
14
 
15
- def relation
16
- model.unscoped
15
+ def collection
16
+ model.unscoped.in_batches(of: 100)
17
17
  end
18
18
 
19
- def process_batch(relation)
19
+ def process(relation)
20
20
  updates = counters.map do |counter_association|
21
21
  has_many_association = has_many_association(counter_association)
22
22
 
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundDataMigrations
5
+ # Class responsible for scheduling background data migrations.
6
+ #
7
+ # Scheduler should be configured to run periodically, for example, via cron.
8
+ #
9
+ # @example Run via whenever
10
+ # # add this to schedule.rb
11
+ # every 1.minute do
12
+ # runner "OnlineMigrations.run_background_data_migrations"
13
+ # end
14
+ #
15
+ # @example Specific shard
16
+ # every 1.minute do
17
+ # runner "OnlineMigrations.run_background_data_migrations(shard: :shard_two)"
18
+ # end
19
+ #
20
+ # @example Custom concurrency
21
+ # every 1.minute do
22
+ # # Allow to run 2 data migrations in parallel.
23
+ # runner "OnlineMigrations.run_background_data_migrations(concurrency: 2)"
24
+ # end
25
+ #
26
+ class Scheduler
27
+ def self.run(**options)
28
+ new.run(**options)
29
+ end
30
+
31
+ # Runs Scheduler
32
+ def run(shard: nil, concurrency: 1)
33
+ relation = Migration.queue_order
34
+ relation = relation.where(shard: shard) if shard
35
+
36
+ with_lock do
37
+ running = relation.running
38
+ enqueued = relation.enqueued
39
+
40
+ # Ensure no more than 'concurrency' migrations are running at the same time.
41
+ remaining_to_enqueue = concurrency - running.count
42
+ if remaining_to_enqueue > 0
43
+ migrations_to_enqueue = enqueued.limit(remaining_to_enqueue)
44
+ migrations_to_enqueue.each do |migration|
45
+ enqueue_migration(migration)
46
+ end
47
+ end
48
+ end
49
+
50
+ true
51
+ end
52
+
53
+ private
54
+ def with_lock(&block)
55
+ # Don't lock the whole table if we can lock only a single record (must be always the same).
56
+ first_record = Migration.queue_order.first
57
+ if first_record
58
+ first_record.with_lock(&block)
59
+ else
60
+ Migration.transaction do
61
+ Migration.connection.execute("LOCK #{Migration.table_name} IN ACCESS EXCLUSIVE MODE")
62
+ yield
63
+ end
64
+ end
65
+ end
66
+
67
+ def enqueue_migration(migration)
68
+ job = OnlineMigrations.config.background_data_migrations.job
69
+ job_class = job.constantize
70
+
71
+ jid = job_class.perform_async(migration.id)
72
+ if jid
73
+ migration.update!(status: :running, jid: jid)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundDataMigrations
5
+ # This class encapsulates the logic behind updating the tick counter.
6
+ #
7
+ # It's initialized with a duration for the throttle, and a block to persist
8
+ # the number of ticks to increment.
9
+ #
10
+ # When +tick+ is called, the block will be called with the increment,
11
+ # provided the duration since the last update (or initialization) has been
12
+ # long enough.
13
+ #
14
+ # To not lose any increments, +persist+ should be used, which may call the
15
+ # block with any leftover ticks.
16
+ #
17
+ # @private
18
+ class Ticker
19
+ # Creates a Ticker that will call the block each time +tick+ is called,
20
+ # unless the tick is being throttled.
21
+ #
22
+ # @param interval [ActiveSupport::Duration, Numeric] Duration
23
+ # since initialization or last call that will cause a throttle.
24
+ # @yieldparam ticks [Integer] the increment in ticks to be persisted.
25
+ #
26
+ def initialize(interval, &block)
27
+ @interval = interval
28
+ @block = block
29
+ @last_persisted_at = Time.current
30
+ @ticks_recorded = 0
31
+ end
32
+
33
+ # Increments the tick count by one, and may persist the new value if the
34
+ # threshold duration has passed since initialization or the tick count was
35
+ # last persisted.
36
+ #
37
+ def tick
38
+ @ticks_recorded += 1
39
+ persist if persist?
40
+ end
41
+
42
+ # Persists the tick increments by calling the block passed to the
43
+ # initializer. This is idempotent in the sense that calling it twice in a
44
+ # row will call the block at most once (if it had been throttled).
45
+ #
46
+ def persist
47
+ return if @ticks_recorded == 0
48
+
49
+ now = Time.current
50
+ duration = now - @last_persisted_at
51
+ @last_persisted_at = now
52
+ @block.call(@ticks_recorded, duration)
53
+ @ticks_recorded = 0
54
+ end
55
+
56
+ private
57
+ def persist?
58
+ Time.now - @last_persisted_at >= @interval
59
+ end
60
+ end
61
+ end
62
+ end
@@ -4,9 +4,9 @@ module OnlineMigrations
4
4
  module BackgroundSchemaMigrations
5
5
  # Class representing configuration options for background schema migrations.
6
6
  class Config
7
- # Maximum number of run attempts
7
+ # Maximum number of run attempts.
8
8
  #
9
- # When attempts are exhausted, the migration is marked as failed.
9
+ # When attempts are exhausted, the schema migration is marked as failed.
10
10
  # @return [Integer] defaults to 5
11
11
  #
12
12
  attr_accessor :max_attempts
@@ -8,13 +8,15 @@ module OnlineMigrations
8
8
  # `enqueue_background_schema_migration` helper inside migrations.
9
9
  #
10
10
  class Migration < ApplicationRecord
11
+ include ShardAware
12
+
11
13
  STATUSES = [
12
- :enqueued, # The migration has been enqueued by the user.
13
- :running, # The migration is being performed by a migration executor.
14
- :errored, # The migration raised an error during last run.
15
- :failed, # The migration raises an error when running and retry attempts exceeded.
16
- :succeeded, # The migration finished without error.
17
- :cancelled, # The migration was cancelled by the user.
14
+ "enqueued", # The migration has been enqueued by the user.
15
+ "running", # The migration is being performed by a migration executor.
16
+ "errored", # The migration raised an error during last run.
17
+ "failed", # The migration raises an error when running and retry attempts exceeded.
18
+ "succeeded", # The migration finished without error.
19
+ "cancelled", # The migration was cancelled by the user.
18
20
  ]
19
21
 
20
22
  MAX_IDENTIFIER_LENGTH = 63
@@ -22,36 +24,12 @@ module OnlineMigrations
22
24
  self.table_name = :background_schema_migrations
23
25
 
24
26
  scope :queue_order, -> { order(created_at: :asc) }
25
- scope :parents, -> { where(parent_id: nil) }
26
- scope :runnable, -> { where(composite: false) }
27
27
  scope :active, -> { where(status: [:enqueued, :running, :errored]) }
28
- scope :except_succeeded, -> { where.not(status: :succeeded) }
29
-
30
- scope :stuck, -> do
31
- runnable.active.where(<<~SQL)
32
- updated_at <= NOW() - interval '1 second' * (COALESCE(statement_timeout, 60*60*24) + 60*10)
33
- SQL
34
- end
35
-
36
- scope :retriable, -> do
37
- stuck_sql = connection.unprepared_statement { stuck.to_sql }
38
-
39
- from(Arel.sql(<<~SQL))
40
- (
41
- (SELECT * FROM background_schema_migrations WHERE NOT composite AND status = 'errored')
42
- UNION
43
- (#{stuck_sql})
44
- ) AS #{table_name}
45
- SQL
46
- end
47
28
 
48
29
  alias_attribute :name, :migration_name
49
30
 
50
31
  enum :status, STATUSES.index_with(&:to_s)
51
32
 
52
- belongs_to :parent, class_name: name, optional: true, inverse_of: :children
53
- has_many :children, class_name: name, foreign_key: :parent_id, inverse_of: :parent
54
-
55
33
  validates :table_name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH }
56
34
  validates :definition, presence: true
57
35
  validates :migration_name, presence: true, uniqueness: {
@@ -65,70 +43,53 @@ module OnlineMigrations
65
43
  end,
66
44
  }
67
45
 
68
- validate :validate_children_statuses, if: -> { composite? && status_changed? }
69
- validate :validate_connection_class, if: :connection_class_name?
70
46
  validate :validate_table_exists
71
47
  validates_with MigrationStatusValidator, on: :update
72
48
 
73
49
  before_validation :set_defaults
74
50
 
51
+ # Returns whether the migration is completed, which is defined as
52
+ # having a status of succeeded, failed, or cancelled.
53
+ #
54
+ # @return [Boolean] whether the migration is completed.
55
+ #
75
56
  def completed?
76
- succeeded? || failed?
57
+ succeeded? || failed? || cancelled?
77
58
  end
78
59
 
79
- # Overwrite enum's generated method to correctly work for composite migrations.
80
- def cancelled!
81
- return super if !composite?
82
-
83
- transaction do
84
- super
85
- children.each { |child| child.cancelled! if !child.succeeded? }
86
- end
60
+ # Returns whether the migration is active, which is defined as
61
+ # having a status of enqueued, or running.
62
+ #
63
+ # @return [Boolean] whether the migration is active.
64
+ #
65
+ def active?
66
+ enqueued? || running?
87
67
  end
68
+
88
69
  alias cancel cancelled!
89
70
 
71
+ # Returns whether this migration is pausable.
72
+ #
90
73
  def pausable?
91
74
  false
92
75
  end
93
76
 
94
- def can_be_paused?
95
- false
96
- end
97
-
98
- def can_be_cancelled?
99
- !succeeded? && !cancelled?
100
- end
101
-
102
- # Returns the progress of the background schema migration.
77
+ # Dummy method to support the same interface as background data migrations.
103
78
  #
104
- # @return [Float] value in range from 0.0 to 100.0
79
+ # @return [nil]
105
80
  #
106
81
  def progress
107
- if succeeded?
108
- 100.0
109
- elsif composite?
110
- progresses = children.map(&:progress)
111
- # There should not be composite migrations without children,
112
- # but children may be deleted for some reason, so we need to
113
- # make a check to avoid 0 division error.
114
- if progresses.any?
115
- (progresses.sum.to_f / progresses.size).round(2)
116
- else
117
- 0.0
118
- end
119
- else
120
- 0.0
121
- end
122
82
  end
123
83
 
124
84
  # Whether the migration is considered stuck (is running for some configured time).
125
85
  #
126
86
  def stuck?
127
- # Composite migrations are not considered stuck.
128
- return false if composite?
129
-
130
- stuck_timeout = (statement_timeout || 1.day) + 10.minutes
131
- running? && updated_at <= stuck_timeout.seconds.ago
87
+ if index_addition?
88
+ running? && !index_build_in_progress?
89
+ else
90
+ stuck_timeout = (statement_timeout || 1.day) + 10.minutes
91
+ running? && updated_at <= stuck_timeout.seconds.ago
92
+ end
132
93
  end
133
94
 
134
95
  # Mark this migration as ready to be processed again.
@@ -136,27 +97,16 @@ module OnlineMigrations
136
97
  # This is used to manually retrying failed migrations.
137
98
  #
138
99
  def retry
139
- if composite? && failed?
140
- transaction do
141
- update!(status: :enqueued, finished_at: nil)
142
- children.failed.each(&:retry)
143
- end
144
-
145
- true
146
- elsif failed?
147
- transaction do
148
- parent.update!(status: :enqueued, finished_at: nil) if parent
149
-
150
- update!(
151
- status: :enqueued,
152
- attempts: 0,
153
- started_at: nil,
154
- finished_at: nil,
155
- error_class: nil,
156
- error_message: nil,
157
- backtrace: nil
158
- )
159
- end
100
+ if failed?
101
+ update!(
102
+ status: :enqueued,
103
+ attempts: 0,
104
+ started_at: nil,
105
+ finished_at: nil,
106
+ error_class: nil,
107
+ error_message: nil,
108
+ backtrace: nil
109
+ )
160
110
 
161
111
  true
162
112
  else
@@ -168,15 +118,6 @@ module OnlineMigrations
168
118
  definition.match?(/create (unique )?index/i)
169
119
  end
170
120
 
171
- # @private
172
- def connection_class
173
- if connection_class_name && (klass = connection_class_name.safe_constantize)
174
- Utils.find_connection_class(klass)
175
- else
176
- ActiveRecord::Base
177
- end
178
- end
179
-
180
121
  # @private
181
122
  def attempts_exceeded?
182
123
  attempts >= max_attempts
@@ -184,7 +125,7 @@ module OnlineMigrations
184
125
 
185
126
  # @private
186
127
  def run
187
- on_shard do
128
+ on_shard_if_present do
188
129
  connection = connection_class.connection
189
130
 
190
131
  connection.with_lock_retries do
@@ -214,28 +155,11 @@ module OnlineMigrations
214
155
  end
215
156
 
216
157
  private
217
- def validate_children_statuses
218
- if composite?
219
- if succeeded? && children.except_succeeded.exists?
220
- errors.add(:base, "all child migrations must be succeeded")
221
- elsif failed? && !children.failed.exists?
222
- errors.add(:base, "at least one child migration must be failed")
223
- end
224
- end
225
- end
226
-
227
- def validate_connection_class
228
- klass = connection_class_name.safe_constantize
229
- if !(klass <= ActiveRecord::Base)
230
- errors.add(:connection_class_name, "is not an ActiveRecord::Base child class")
231
- end
232
- end
233
-
234
158
  def validate_table_exists
235
159
  # Skip this validation if we have invalid connection class name.
236
160
  return if errors.include?(:connection_class_name)
237
161
 
238
- on_shard do
162
+ on_shard_if_present do
239
163
  if !connection_class.connection.table_exists?(table_name)
240
164
  errors.add(:table_name, "'#{table_name}' does not exist")
241
165
  end
@@ -248,9 +172,13 @@ module OnlineMigrations
248
172
  self.statement_timeout ||= config.statement_timeout
249
173
  end
250
174
 
251
- def on_shard(&block)
252
- shard = (self.shard || connection_class.default_shard).to_sym
253
- connection_class.connected_to(shard: shard, role: :writing, &block)
175
+ def index_build_in_progress?
176
+ indexes_in_progress = connection_class.connection.select_values(<<~SQL)
177
+ SELECT index_relid::regclass::text
178
+ FROM pg_stat_progress_create_index
179
+ SQL
180
+
181
+ indexes_in_progress.include?(name)
254
182
  end
255
183
 
256
184
  def with_statement_timeout(connection, timeout)