online_migrations 0.14.1 → 0.16.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +13 -9
  4. data/docs/{background_migrations.md → background_data_migrations.md} +26 -40
  5. data/docs/background_schema_migrations.md +163 -0
  6. data/docs/configuring.md +35 -4
  7. data/lib/generators/online_migrations/background_migration_generator.rb +12 -1
  8. data/lib/generators/online_migrations/install_generator.rb +1 -1
  9. data/lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt +29 -0
  10. data/lib/generators/online_migrations/templates/initializer.rb.tt +24 -11
  11. data/lib/generators/online_migrations/templates/install_migration.rb.tt +75 -0
  12. data/lib/generators/online_migrations/templates/migration.rb.tt +7 -48
  13. data/lib/generators/online_migrations/upgrade_generator.rb +5 -0
  14. data/lib/online_migrations/application_record.rb +11 -0
  15. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -7
  16. data/lib/online_migrations/background_migrations/config.rb +27 -24
  17. data/lib/online_migrations/background_migrations/delete_associated_records.rb +1 -1
  18. data/lib/online_migrations/background_migrations/migration.rb +1 -8
  19. data/lib/online_migrations/background_migrations/migration_helpers.rb +16 -2
  20. data/lib/online_migrations/background_migrations/migration_job.rb +5 -2
  21. data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -1
  22. data/lib/online_migrations/background_migrations/migration_runner.rb +1 -1
  23. data/lib/online_migrations/background_schema_migrations/config.rb +40 -0
  24. data/lib/online_migrations/background_schema_migrations/migration.rb +205 -0
  25. data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +76 -0
  26. data/lib/online_migrations/background_schema_migrations/migration_runner.rb +110 -0
  27. data/lib/online_migrations/background_schema_migrations/migration_status_validator.rb +33 -0
  28. data/lib/online_migrations/background_schema_migrations/scheduler.rb +30 -0
  29. data/lib/online_migrations/command_checker.rb +31 -1
  30. data/lib/online_migrations/command_recorder.rb +5 -0
  31. data/lib/online_migrations/config.rb +32 -0
  32. data/lib/online_migrations/error_messages.rb +3 -3
  33. data/lib/online_migrations/lock_retrier.rb +0 -2
  34. data/lib/online_migrations/schema_statements.rb +1 -0
  35. data/lib/online_migrations/utils.rb +12 -0
  36. data/lib/online_migrations/version.rb +1 -1
  37. data/lib/online_migrations.rb +19 -2
  38. metadata +13 -4
  39. data/lib/online_migrations/background_migrations/application_record.rb +0 -13
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundSchemaMigrations
5
+ # Runs single background schema migration.
6
+ class MigrationRunner
7
+ attr_reader :migration
8
+
9
+ def initialize(migration)
10
+ @migration = migration
11
+ end
12
+
13
+ def run
14
+ mark_as_running if migration.enqueued? || migration.failed?
15
+
16
+ if migration.composite?
17
+ migration.children.each do |child_migration|
18
+ runner = self.class.new(child_migration)
19
+ runner.run
20
+ end
21
+ else
22
+ do_run
23
+ end
24
+ end
25
+
26
+ private
27
+ def mark_as_running
28
+ Migration.transaction do
29
+ migration.running!
30
+ migration.parent.running! if migration.parent
31
+ end
32
+ end
33
+
34
+ def do_run
35
+ migration_payload = notifications_payload(migration)
36
+
37
+ if migration.attempts == 0
38
+ ActiveSupport::Notifications.instrument("started.background_schema_migrations", migration_payload)
39
+ else
40
+ ActiveSupport::Notifications.instrument("retried.background_schema_migrations", migration_payload)
41
+ end
42
+
43
+ if should_throttle?
44
+ ActiveSupport::Notifications.instrument("throttled.background_schema_migrations", migration_payload)
45
+ return
46
+ end
47
+
48
+ migration.update!(
49
+ attempts: migration.attempts + 1,
50
+ status: :running,
51
+ started_at: Time.current,
52
+ finished_at: nil,
53
+ error_class: nil,
54
+ error_message: nil,
55
+ backtrace: nil
56
+ )
57
+
58
+ ActiveSupport::Notifications.instrument("run.background_schema_migrations", migration_payload) do
59
+ migration.run
60
+ end
61
+
62
+ migration.update!(status: :succeeded, finished_at: Time.current)
63
+
64
+ ActiveSupport::Notifications.instrument("completed.background_schema_migrations", migration_payload)
65
+
66
+ complete_parent_if_needed(migration) if migration.parent.present?
67
+ rescue Exception => e # rubocop:disable Lint/RescueException
68
+ backtrace_cleaner = ::OnlineMigrations.config.backtrace_cleaner
69
+
70
+ migration.update!(
71
+ status: :failed,
72
+ finished_at: Time.current,
73
+ error_class: e.class.name,
74
+ error_message: e.message,
75
+ backtrace: backtrace_cleaner ? backtrace_cleaner.clean(e.backtrace) : e.backtrace
76
+ )
77
+
78
+ ::OnlineMigrations.config.background_schema_migrations.error_handler.call(e, migration)
79
+ end
80
+
81
+ def should_throttle?
82
+ ::OnlineMigrations.config.throttler.call
83
+ end
84
+
85
+ def complete_parent_if_needed(migration)
86
+ parent = migration.parent
87
+ completed = false
88
+
89
+ parent.with_lock do
90
+ children = parent.children.select(:status)
91
+ if children.all?(&:succeeded?)
92
+ parent.succeeded!
93
+ completed = true
94
+ elsif children.any?(&:failed?)
95
+ parent.failed!
96
+ completed = true
97
+ end
98
+ end
99
+
100
+ if completed
101
+ ActiveSupport::Notifications.instrument("completed.background_migrations", notifications_payload(migration))
102
+ end
103
+ end
104
+
105
+ def notifications_payload(migration)
106
+ { background_schema_migration: migration }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundSchemaMigrations
5
+ # @private
6
+ class MigrationStatusValidator < ActiveModel::Validator
7
+ VALID_STATUS_TRANSITIONS = {
8
+ # enqueued -> running occurs when the migration starts performing.
9
+ "enqueued" => ["running"],
10
+ # running -> succeeded occurs when the migration completes successfully.
11
+ # running -> failed occurs when the migration raises an exception when running and retry attempts exceeded.
12
+ "running" => ["succeeded", "failed"],
13
+ # failed -> enqueued occurs when the failed migration is enqueued to be retried.
14
+ # failed -> running occurs when the failed migration is retried.
15
+ "failed" => ["enqueued", "running"],
16
+ }
17
+
18
+ def validate(record)
19
+ return if !record.status_changed?
20
+
21
+ previous_status, new_status = record.status_change
22
+ valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
23
+
24
+ if !valid_new_statuses.include?(new_status)
25
+ record.errors.add(
26
+ :status,
27
+ "cannot transition background schema migration from status #{previous_status} to #{new_status}"
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundSchemaMigrations
5
+ # Class responsible for scheduling background schema migrations.
6
+ # It selects a single migration and runs it if there is no currently running migration.
7
+ #
8
+ # Scheduler should be configured to run periodically, for example, via cron.
9
+ # @example Run via whenever
10
+ # # add this to schedule.rb
11
+ # every 1.minute do
12
+ # runner "OnlineMigrations.run_background_schema_migrations"
13
+ # end
14
+ #
15
+ class Scheduler
16
+ def self.run
17
+ new.run
18
+ end
19
+
20
+ # Runs Scheduler
21
+ def run
22
+ migration = Migration.runnable.enqueued.queue_order.first || Migration.retriable.queue_order.first
23
+ if migration
24
+ runner = MigrationRunner.new(migration)
25
+ runner.run
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -527,6 +527,30 @@ module OnlineMigrations
527
527
  end
528
528
  alias add_belongs_to add_reference
529
529
 
530
+ def add_reference_concurrently(table_name, ref_name, **options)
531
+ # Always added by default in 5.0+
532
+ index = options.fetch(:index, true)
533
+
534
+ if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
535
+ raise_error :add_hash_index
536
+ end
537
+
538
+ foreign_key = options.fetch(:foreign_key, false)
539
+
540
+ if foreign_key
541
+ foreign_table_name = Utils.foreign_table_name(ref_name, options)
542
+ @foreign_key_tables << foreign_table_name.to_s
543
+ end
544
+
545
+ if !options[:polymorphic]
546
+ type = (options[:type] || :bigint).to_sym
547
+ column_name = "#{ref_name}_id"
548
+
549
+ foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
550
+ check_mismatched_foreign_key_type(table_name, column_name, type, **foreign_key_options)
551
+ end
552
+ end
553
+
530
554
  def add_index(table_name, column_name, **options)
531
555
  if options[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
532
556
  raise_error :add_hash_index
@@ -830,8 +854,14 @@ module OnlineMigrations
830
854
  if connection.table_exists?(foreign_table_name)
831
855
  primary_key = options[:primary_key] || connection.primary_key(foreign_table_name)
832
856
  primary_key_column = column_for(foreign_table_name, primary_key)
857
+ return if primary_key_column.nil?
858
+
859
+ primary_key_type = primary_key_column.sql_type.to_sym
860
+ # Having bigint foreign keys is safe and people should
861
+ # detect integer primary keys via some other tools.
862
+ return if type == :bigint && primary_key_type == :integer
833
863
 
834
- if primary_key_column && type != primary_key_column.sql_type.to_sym
864
+ if type != primary_key_type
835
865
  raise_error :mismatched_foreign_key_type,
836
866
  table_name: table_name, column_name: column_name
837
867
  end
@@ -26,6 +26,7 @@ module OnlineMigrations
26
26
  :add_reference_concurrently,
27
27
  :change_column_type_in_background,
28
28
  :enqueue_background_migration,
29
+ :remove_background_migration,
29
30
 
30
31
  # column type change helpers
31
32
  :initialize_column_type_change,
@@ -77,6 +78,10 @@ module OnlineMigrations
77
78
 
78
79
  include StraightReversions
79
80
 
81
+ def invert_add_reference_concurrently(args)
82
+ [:remove_reference, args]
83
+ end
84
+
80
85
  def invert_swap_column_names(args)
81
86
  table_name, column1, column2 = args
82
87
  [:swap_column_names, [table_name, column2, column1]]
@@ -181,6 +181,26 @@ module OnlineMigrations
181
181
  #
182
182
  attr_accessor :run_background_migrations_inline
183
183
 
184
+ # Allows to throttle background data or schema migrations based on external signal (e.g. database health)
185
+ #
186
+ # It will be called before each run.
187
+ # If throttled, the current run will be retried next time.
188
+ #
189
+ # @return [Proc]
190
+ #
191
+ # @example
192
+ # OnlineMigrations.config.throttler = -> { DatabaseStatus.unhealthy? }
193
+ #
194
+ attr_reader :throttler
195
+
196
+ # The Active Support backtrace cleaner that will be used to clean the
197
+ # backtrace of a background data or schema migration that errors.
198
+ #
199
+ # @return [ActiveSupport::BacktraceCleaner, nil] the backtrace cleaner to
200
+ # use when cleaning a background migrations's backtrace. Defaults to `Rails.backtrace_cleaner`
201
+ #
202
+ attr_accessor :backtrace_cleaner
203
+
184
204
  # Configuration object to configure background migrations
185
205
  #
186
206
  # @return [BackgroundMigrationsConfig]
@@ -188,6 +208,8 @@ module OnlineMigrations
188
208
  #
189
209
  attr_reader :background_migrations
190
210
 
211
+ attr_reader :background_schema_migrations
212
+
191
213
  def initialize
192
214
  @table_renames = {}
193
215
  @column_renames = {}
@@ -202,6 +224,7 @@ module OnlineMigrations
202
224
  )
203
225
 
204
226
  @background_migrations = BackgroundMigrations::Config.new
227
+ @background_schema_migrations = BackgroundSchemaMigrations::Config.new
205
228
 
206
229
  @checks = []
207
230
  @start_after = 0
@@ -213,6 +236,7 @@ module OnlineMigrations
213
236
  @enabled_checks = @error_messages.keys.index_with({})
214
237
  @verbose_sql_logs = defined?(Rails.env) && (Rails.env.production? || Rails.env.staging?)
215
238
  @run_background_migrations_inline = -> { Utils.developer_env? }
239
+ @throttler = -> { false }
216
240
  end
217
241
 
218
242
  def lock_retrier=(value)
@@ -223,6 +247,14 @@ module OnlineMigrations
223
247
  @small_tables = table_names.map(&:to_s)
224
248
  end
225
249
 
250
+ def throttler=(value)
251
+ if !value.respond_to?(:call)
252
+ raise ArgumentError, "throttler must be a callable."
253
+ end
254
+
255
+ @throttler = value
256
+ end
257
+
226
258
  # Enables specific check
227
259
  #
228
260
  # For the list of available checks look at the `error_messages.rb` file.
@@ -110,7 +110,7 @@ A safer approach is to:
110
110
  1. ignore the column:
111
111
 
112
112
  class <%= model %> < ApplicationRecord
113
- self.ignored_columns = [\"<%= column_name %>\"]
113
+ self.ignored_columns += [\"<%= column_name %>\"]
114
114
  end
115
115
 
116
116
  2. deploy
@@ -151,7 +151,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
151
151
  <% if enumerate_columns_in_select_statements %>
152
152
  5. Ignore old column
153
153
 
154
- self.ignored_columns = [:<%= column_name %>]
154
+ self.ignored_columns += [:<%= column_name %>]
155
155
 
156
156
  6. Deploy
157
157
  7. Remove the column rename config from step 1
@@ -298,7 +298,7 @@ A safer approach is to:
298
298
  1. Ignore the column:
299
299
 
300
300
  class <%= model %> < ApplicationRecord
301
- self.ignored_columns = <%= columns %>
301
+ self.ignored_columns += <%= columns %>
302
302
  end
303
303
 
304
304
  2. Deploy
@@ -229,11 +229,9 @@ module OnlineMigrations
229
229
  end
230
230
 
231
231
  def lock_timeout(*)
232
- 0
233
232
  end
234
233
 
235
234
  def delay(*)
236
- 0
237
235
  end
238
236
 
239
237
  def with_lock_retries
@@ -4,6 +4,7 @@ module OnlineMigrations
4
4
  module SchemaStatements
5
5
  include ChangeColumnTypeHelpers
6
6
  include BackgroundMigrations::MigrationHelpers
7
+ include BackgroundSchemaMigrations::MigrationHelpers
7
8
 
8
9
  # Updates the value of a column in batches.
9
10
  #
@@ -135,6 +135,13 @@ module OnlineMigrations
135
135
  connection.select_value(query) == "v"
136
136
  end
137
137
 
138
+ def find_connection_class(model)
139
+ model.ancestors.find do |parent|
140
+ parent == ActiveRecord::Base ||
141
+ (parent.is_a?(Class) && parent.abstract_class?)
142
+ end
143
+ end
144
+
138
145
  def shard_names(model)
139
146
  model.ancestors.each do |ancestor|
140
147
  # There is no official method to get shard names from the model.
@@ -151,6 +158,11 @@ module OnlineMigrations
151
158
  db_config = ActiveRecord::Base.configurations.configs_for(env_name: env)
152
159
  db_config.reject(&:replica?).size > 1
153
160
  end
161
+
162
+ def run_background_migrations_inline?
163
+ run_inline = OnlineMigrations.config.run_background_migrations_inline
164
+ run_inline && run_inline.call
165
+ end
154
166
  end
155
167
  end
156
168
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.14.1"
4
+ VERSION = "0.16.0"
5
5
  end
@@ -4,6 +4,7 @@ require "active_record"
4
4
 
5
5
  require "online_migrations/version"
6
6
  require "online_migrations/utils"
7
+ require "online_migrations/background_schema_migrations/migration_helpers"
7
8
  require "online_migrations/change_column_type_helpers"
8
9
  require "online_migrations/background_migrations/migration_helpers"
9
10
  require "online_migrations/schema_statements"
@@ -22,6 +23,7 @@ module OnlineMigrations
22
23
 
23
24
  extend ActiveSupport::Autoload
24
25
 
26
+ autoload :ApplicationRecord
25
27
  autoload :BatchIterator
26
28
  autoload :VerboseSqlLogs
27
29
  autoload :ForeignKeysCollector
@@ -52,7 +54,6 @@ module OnlineMigrations
52
54
  autoload :DeleteOrphanedRecords
53
55
  autoload :PerformActionOnRelation
54
56
  autoload :ResetCounters
55
- autoload :ApplicationRecord
56
57
  autoload :MigrationJob
57
58
  autoload :Migration
58
59
  autoload :MigrationJobRunner
@@ -60,6 +61,16 @@ module OnlineMigrations
60
61
  autoload :Scheduler
61
62
  end
62
63
 
64
+ module BackgroundSchemaMigrations
65
+ extend ActiveSupport::Autoload
66
+
67
+ autoload :Config
68
+ autoload :Migration
69
+ autoload :MigrationStatusValidator
70
+ autoload :MigrationRunner
71
+ autoload :Scheduler
72
+ end
73
+
63
74
  class << self
64
75
  # @private
65
76
  attr_accessor :current_migration
@@ -72,10 +83,16 @@ module OnlineMigrations
72
83
  @config ||= Config.new
73
84
  end
74
85
 
75
- # Run background migrations
86
+ # Run background data migrations
76
87
  def run_background_migrations
77
88
  BackgroundMigrations::Scheduler.run
78
89
  end
90
+ alias run_background_data_migrations run_background_migrations
91
+
92
+ # Run background schema migrations
93
+ def run_background_schema_migrations
94
+ BackgroundSchemaMigrations::Scheduler.run
95
+ end
79
96
 
80
97
  def deprecator
81
98
  @deprecator ||=
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-21 00:00:00.000000000 Z
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -34,18 +34,21 @@ files:
34
34
  - CHANGELOG.md
35
35
  - LICENSE.txt
36
36
  - README.md
37
- - docs/background_migrations.md
37
+ - docs/background_data_migrations.md
38
+ - docs/background_schema_migrations.md
38
39
  - docs/configuring.md
39
40
  - lib/generators/online_migrations/background_migration_generator.rb
40
41
  - lib/generators/online_migrations/install_generator.rb
41
42
  - lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt
42
43
  - lib/generators/online_migrations/templates/background_migration.rb.tt
44
+ - lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt
43
45
  - lib/generators/online_migrations/templates/initializer.rb.tt
46
+ - lib/generators/online_migrations/templates/install_migration.rb.tt
44
47
  - lib/generators/online_migrations/templates/migration.rb.tt
45
48
  - lib/generators/online_migrations/upgrade_generator.rb
46
49
  - lib/online_migrations.rb
50
+ - lib/online_migrations/application_record.rb
47
51
  - lib/online_migrations/background_migration.rb
48
- - lib/online_migrations/background_migrations/application_record.rb
49
52
  - lib/online_migrations/background_migrations/backfill_column.rb
50
53
  - lib/online_migrations/background_migrations/background_migration_class_validator.rb
51
54
  - lib/online_migrations/background_migrations/config.rb
@@ -62,6 +65,12 @@ files:
62
65
  - lib/online_migrations/background_migrations/perform_action_on_relation.rb
63
66
  - lib/online_migrations/background_migrations/reset_counters.rb
64
67
  - lib/online_migrations/background_migrations/scheduler.rb
68
+ - lib/online_migrations/background_schema_migrations/config.rb
69
+ - lib/online_migrations/background_schema_migrations/migration.rb
70
+ - lib/online_migrations/background_schema_migrations/migration_helpers.rb
71
+ - lib/online_migrations/background_schema_migrations/migration_runner.rb
72
+ - lib/online_migrations/background_schema_migrations/migration_status_validator.rb
73
+ - lib/online_migrations/background_schema_migrations/scheduler.rb
65
74
  - lib/online_migrations/batch_iterator.rb
66
75
  - lib/online_migrations/change_column_type_helpers.rb
67
76
  - lib/online_migrations/command_checker.rb
@@ -1,13 +0,0 @@
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