├── .github ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── activerecord-safer_migrations.gemspec ├── lib ├── active_record │ └── safer_migrations │ │ ├── migration.rb │ │ ├── postgresql_adapter.rb │ │ ├── railtie.rb │ │ ├── setting_helper.rb │ │ └── version.rb └── activerecord-safer_migrations.rb └── spec ├── active_record └── safer_migrations │ └── migration_spec.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | rubocop: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | bundler-cache: true 21 | - run: bundle exec rubocop --extra-details --display-style-guide --parallel --force-exclusion 22 | 23 | tests: 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | ruby-version: ["3.1", "3.2", "3.3"] 28 | activerecord-version: 29 | - "7.0.8" 30 | - "7.1.3.4" 31 | - "7.2.0" 32 | runs-on: ubuntu-latest 33 | services: 34 | postgres: 35 | image: postgres:14 36 | env: 37 | POSTGRES_USER: postgres 38 | POSTGRES_DB: safer_migrations_test 39 | POSTGRES_PASSWORD: safer_migrations 40 | ports: 41 | - 5432:5432 42 | options: >- 43 | --health-cmd pg_isready 44 | --health-interval 10s 45 | --health-timeout 5s 46 | --health-retries 10 47 | env: 48 | DATABASE_URL: postgres://postgres:safer_migrations@localhost/safer_migrations_test 49 | DATABASE_DEPENDENCY_PORT: "5432" 50 | ACTIVERECORD_VERSION: "${{ matrix.activerecord-version }}" 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Set up Ruby 54 | uses: ruby/setup-ruby@v1 55 | with: 56 | bundler-cache: true 57 | ruby-version: "${{ matrix.ruby-version }}" 58 | - name: Run specs 59 | run: | 60 | bundle exec rspec --profile --format progress 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | Gemfile.lock 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | gc_ruboconfig: rubocop.yml 3 | 4 | AllCops: 5 | TargetRubyVersion: 3.2 6 | NewCops: enable 7 | 8 | Gemspec/RequiredRubyVersion: 9 | Enabled: false 10 | 11 | Naming/FileName: 12 | Exclude: 13 | - lib/activerecord-safer_migrations.rb 14 | 15 | Style/GlobalVars: 16 | Exclude: 17 | - "spec/active_record/safer_migrations/migration_spec.rb" 18 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.4 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0.0 / 2024-08-21 2 | 3 | - Remove support for Ruby =< 3.0 and Rails < 7.0 4 | - Add support for ActiveRecord 7.2 (#98) 5 | 6 | # 3.0.0 / 2020-09-28 7 | 8 | - [#55](https://github.com/gocardless/activerecord-safer_migrations/pull/55) Drop support for Ruby =< 2.4 and Rails =< 5.1 9 | 10 | # 2.0.0 / 2017-08-23 11 | 12 | - [#23](https://github.com/gocardless/activerecord-safer_migrations/pull/23) Drop support for Rails 4.0 and 4.1 13 | - [#24](https://github.com/gocardless/activerecord-safer_migrations/pull/24) Drop support for Ruby 2.0 and 2.1, add Ruby 2.4 14 | 15 | # 1.0.0 / 2016-05-09 16 | 17 | - Support for Rails 5 18 | 19 | # 0.1.0 / 2015-12-15 20 | 21 | - Initial public release 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "activerecord", "~> #{ENV['ACTIVERECORD_VERSION']}" if ENV["ACTIVERECORD_VERSION"] 8 | 9 | group :test, :development do 10 | gem "gc_ruboconfig", "~> 5.0" 11 | gem "pg", "~> 1.4" 12 | gem "rspec", "~> 3.13.0" 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 GoCardless Ltd. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ActiveRecord safer migration helpers 2 | 3 | > Looking for Rails 4.0 or Ruby 2.0 support? Please check out the [1.x tree](https://github.com/gocardless/activerecord-safer_migrations/tree/v1.0.0). 4 | 5 | *Note: this library only supports PostgreSQL 9.3+. If you're interested in adding support for other databases, we're open to pull requests!* 6 | 7 | Postgres holds ACCESS EXCLUSIVE locks for [almost all][pg-alter-table] DDL 8 | operations. ACCESS EXCLUSIVE locks conflict with all other table-level locks, 9 | which can cause issues in several situations. For instance: 10 | 11 | 1. If the lock is held for a long time, all other access to the table will be 12 | blocked, which can result in downtime. 13 | 2. Even if the lock is only held briefly, it will block all other access to the 14 | table while it is in the lock queue, as it conflicts with all other locks. 15 | The lock can't be acquired until all other queries ahead of it have finished, 16 | so having to wait on long-running queries can also result in downtime. 17 | See [here][blog-post] for more details. 18 | 19 | Both these issues can be avoided by setting timeouts on the migration connection - 20 | `statement_timeout` and `lock_timeout` respectively. 21 | 22 | Once this gem is loaded, all migrations will automatically have a 23 | `lock_timeout` and a `statement_timeout` set. The initial `lock_timeout` 24 | default is 750ms, and the initial `statement_timeout` default is 1500ms. Both 25 | defaults can be easily changed (e.g. in a Rails initializer). 26 | 27 | ```ruby 28 | ActiveRecord::SaferMigrations.default_lock_timeout = 1000 29 | ActiveRecord::SaferMigrations.default_statement_timeout = 2000 30 | ``` 31 | 32 | To explicitly set timeouts for a given migration, use the `set_lock_timeout` and 33 | `set_statement_timeout` class methods in the migration. 34 | 35 | ```ruby 36 | class LockTest < ActiveRecord::Migration 37 | set_lock_timeout(250) 38 | set_statement_timeout(750) 39 | 40 | def change 41 | create_table :lock_test 42 | end 43 | end 44 | ``` 45 | 46 | To disable timeouts for a migration, use the `disable_lock_timeout!` and 47 | `disable_statement_timeout!` class methods. Note that this is [extremely 48 | dangerous][blog-post] if you're doing any schema alterations in your migration. 49 | 50 | ```ruby 51 | class LockTest < ActiveRecord::Migration 52 | # Only do this if you really know what you're doing! 53 | disable_lock_timeout! 54 | disable_statement_timeout! 55 | 56 | def change 57 | create_table :lock_test 58 | end 59 | end 60 | ``` 61 | 62 | ### Use with PgBouncer 63 | 64 | This gem sets session-level settings on Postgres connections. If you're using 65 | PgBouncer in transaction pooling mode, using session-level settings is 66 | dangerous, as you can't guarantee which connection will receive the setting. 67 | For this reason, this gem is incompatible with transaction-pooling and should 68 | only be used if migrations are run on connections that support session-level 69 | features. 70 | 71 | [blog-post]: https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/ 72 | [pg-alter-table]: http://www.postgresql.org/docs/9.4/static/sql-altertable.html 73 | 74 | -------------------------------------------------------------------------------- /activerecord-safer_migrations.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("lib/active_record/safer_migrations/version", __dir__) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "activerecord-safer_migrations" 7 | gem.version = ActiveRecord::SaferMigrations::VERSION 8 | gem.summary = "ActiveRecord migration helpers to avoid downtime" 9 | gem.description = "" 10 | gem.authors = ["GoCardless Engineering"] 11 | gem.email = "developers@gocardless.com" 12 | gem.files = `git ls-files`.split("\n") 13 | gem.require_paths = ["lib"] 14 | gem.homepage = "https://github.com/gocardless/activerecord-safer_migrations" 15 | gem.license = "MIT" 16 | 17 | gem.required_ruby_version = ">= 3.1" 18 | 19 | gem.add_dependency "activerecord", ">= 7.0" 20 | gem.metadata["rubygems_mfa_required"] = "true" 21 | end 22 | -------------------------------------------------------------------------------- /lib/active_record/safer_migrations/migration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record/safer_migrations/setting_helper" 4 | 5 | module ActiveRecord 6 | module SaferMigrations 7 | module Migration 8 | def self.included(base) 9 | base.class_eval do 10 | # Use Rails' class_attribute to get an attribute that you can 11 | # override in subclasses 12 | class_attribute :lock_timeout 13 | class_attribute :statement_timeout 14 | 15 | prepend(InstanceMethods) 16 | extend(ClassMethods) 17 | end 18 | end 19 | 20 | module InstanceMethods 21 | def exec_migration(conn, direction) 22 | # lock_timeout is an instance accessor created by class_attribute 23 | lock_timeout_ms = lock_timeout || SaferMigrations.default_lock_timeout 24 | statement_timeout_ms = statement_timeout || SaferMigrations. 25 | default_statement_timeout 26 | SettingHelper.new(conn, :lock_timeout, lock_timeout_ms).with_setting do 27 | SettingHelper.new(conn, 28 | :statement_timeout, 29 | statement_timeout_ms).with_setting do 30 | super(conn, direction) 31 | end 32 | end 33 | end 34 | end 35 | 36 | module ClassMethods 37 | # rubocop:disable Naming/AccessorMethodName 38 | def set_lock_timeout(timeout) 39 | # rubocop:enable Naming/AccessorMethodName 40 | if timeout.zero? 41 | raise "Setting lock_timeout to 0 is dangerous - it disables the lock " \ 42 | "timeout rather than instantly timing out. If you *actually* " \ 43 | "want to disable the lock timeout (not recommended!), use the " \ 44 | "`disable_lock_timeout!` method." 45 | end 46 | self.lock_timeout = timeout 47 | end 48 | 49 | def disable_lock_timeout! 50 | say "WARNING: disabling the lock timeout. This is very dangerous." 51 | self.lock_timeout = 0 52 | end 53 | 54 | # rubocop:disable Naming/AccessorMethodName 55 | def set_statement_timeout(timeout) 56 | # rubocop:enable Naming/AccessorMethodName 57 | if timeout.zero? 58 | raise "Setting statement_timeout to 0 is dangerous - it disables the " \ 59 | "statement timeout rather than instantly timing out. If you " \ 60 | "*actually* want to disable the statement timeout (not recommended!)" \ 61 | ", use the `disable_statement_timeout!` method." 62 | end 63 | self.statement_timeout = timeout 64 | end 65 | 66 | def disable_statement_timeout! 67 | say "WARNING: disabling the statement timeout. This is very dangerous." 68 | self.statement_timeout = 0 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/active_record/safer_migrations/postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module SaferMigrations 5 | module PostgreSQLAdapter 6 | SET_SETTING_SQL = <<-SQL 7 | UPDATE 8 | pg_settings 9 | SET 10 | setting = :value 11 | WHERE 12 | name = :setting_name 13 | SQL 14 | 15 | GET_SETTING_SQL = <<-SQL 16 | SELECT 17 | setting 18 | FROM 19 | pg_settings 20 | WHERE 21 | name = :setting_name 22 | SQL 23 | 24 | def set_setting(setting_name, value) 25 | sql = fill_sql_values(SET_SETTING_SQL, value: value, setting_name: setting_name) 26 | execute(sql) 27 | end 28 | 29 | def get_setting(setting_name) 30 | sql = fill_sql_values(GET_SETTING_SQL, setting_name: setting_name) 31 | result = execute(sql) 32 | result.first["setting"] 33 | end 34 | 35 | def fill_sql_values(sql, values) 36 | if ActiveRecord.version >= "7.2.0" 37 | ActiveRecord::Base.send(:replace_named_bind_variables, self, sql, values) 38 | else 39 | ActiveRecord::Base.send(:replace_named_bind_variables, sql, values) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/active_record/safer_migrations/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module SaferMigrations 5 | class Railtie < Rails::Railtie 6 | initializer "active_record_safer_migrations.load_adapter" do 7 | ActiveSupport.on_load :active_record do 8 | ActiveRecord::SaferMigrations.load 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/active_record/safer_migrations/setting_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module SaferMigrations 5 | class SettingHelper 6 | def initialize(connection, setting_name, value) 7 | @connection = connection 8 | @setting_name = setting_name 9 | @value = value 10 | end 11 | 12 | # We're changing a connection level setting, and we need to make sure we return 13 | # it to the original value. It is automatically reverted if set within a 14 | # transaction which rolls back, so that case needs handling differently. 15 | # 16 | # | In Transaction | Not in transaction 17 | # --------------------------------------------------------- 18 | # Raises | Reset setting | Reset setting 19 | # Doesn't raise | Don't reset setting | Reset setting 20 | def with_setting 21 | record_current_setting 22 | set_new_setting 23 | yield 24 | reset_setting 25 | rescue StandardError 26 | reset_setting unless in_transaction? 27 | raise 28 | end 29 | 30 | private 31 | 32 | def record_current_setting 33 | @original_value = @connection.get_setting(@setting_name) 34 | end 35 | 36 | def set_new_setting 37 | puts "-- set_setting(#{@setting_name.inspect}, #{@value})" 38 | @connection.set_setting(@setting_name, @value) 39 | end 40 | 41 | def reset_setting 42 | puts "-- set_setting(#{@setting_name.inspect}, #{@original_value})" 43 | @connection.set_setting(@setting_name, @original_value) 44 | end 45 | 46 | def in_transaction? 47 | ActiveRecord::Base.connection.open_transactions.positive? 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/active_record/safer_migrations/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module SaferMigrations 5 | VERSION = "4.0.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/activerecord-safer_migrations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record/connection_adapters/postgresql_adapter" 4 | require "active_record/safer_migrations/postgresql_adapter" 5 | require "active_record/safer_migrations/migration" 6 | 7 | module ActiveRecord 8 | module SaferMigrations 9 | @default_lock_timeout = 750 10 | @default_statement_timeout = 1500 11 | 12 | def self.default_lock_timeout 13 | @default_lock_timeout 14 | end 15 | 16 | def self.default_lock_timeout=(timeout_ms) 17 | @default_lock_timeout = timeout_ms 18 | end 19 | 20 | def self.default_statement_timeout 21 | @default_statement_timeout 22 | end 23 | 24 | def self.default_statement_timeout=(timeout_ms) 25 | @default_statement_timeout = timeout_ms 26 | end 27 | 28 | def self.load 29 | ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do 30 | include ActiveRecord::SaferMigrations::PostgreSQLAdapter 31 | end 32 | 33 | ActiveRecord::Migration.class_eval do 34 | include ActiveRecord::SaferMigrations::Migration 35 | end 36 | end 37 | end 38 | end 39 | 40 | require "active_record/safer_migrations/railtie" if defined?(Rails) 41 | -------------------------------------------------------------------------------- /spec/active_record/safer_migrations/migration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe ActiveRecord::SaferMigrations::Migration do 6 | let(:migration_base_class) do 7 | if ActiveRecord.version >= Gem::Version.new("5.0") 8 | ActiveRecord::Migration[ActiveRecord::Migration.current_version] 9 | else 10 | ActiveRecord::Migration 11 | end 12 | end 13 | 14 | before do 15 | nuke_migrations 16 | TimeoutTestHelpers.set(:lock_timeout, 0) 17 | TimeoutTestHelpers.set(:statement_timeout, 0) 18 | end 19 | 20 | describe "setting timeouts explicitly" do 21 | before do 22 | $lock_timeout = nil 23 | $statement_timeout = nil 24 | end 25 | 26 | shared_examples_for "running the migration" do 27 | let(:migration) do 28 | Class.new(migration_base_class) do 29 | set_lock_timeout(5000) 30 | set_statement_timeout(5001) 31 | 32 | def change 33 | $lock_timeout = TimeoutTestHelpers.get(:lock_timeout) 34 | $statement_timeout = TimeoutTestHelpers.get(:statement_timeout) 35 | end 36 | end 37 | end 38 | 39 | it "sets the lock timeout for the duration of the migration" do 40 | silence_stream($stdout) { run_migration.call } 41 | expect($lock_timeout).to eq(5000) 42 | end 43 | 44 | it "unsets the lock timeout after the migration" do 45 | silence_stream($stdout) { run_migration.call } 46 | expect(TimeoutTestHelpers.get(:lock_timeout)).to eq(0) 47 | end 48 | 49 | it "sets the statement timeout for the duration of the migration" do 50 | silence_stream($stdout) { run_migration.call } 51 | expect($statement_timeout).to eq(5001) 52 | end 53 | 54 | it "unsets the statement timeout after the migration" do 55 | silence_stream($stdout) { run_migration.call } 56 | expect(TimeoutTestHelpers.get(:statement_timeout)).to eq(0) 57 | end 58 | 59 | context "when the original timeout is not 0" do 60 | before do 61 | TimeoutTestHelpers.set(:lock_timeout, 8000) 62 | TimeoutTestHelpers.set(:statement_timeout, 8001) 63 | end 64 | 65 | it "unsets the lock timeout after the migration" do 66 | silence_stream($stdout) { run_migration.call } 67 | expect(TimeoutTestHelpers.get(:lock_timeout)).to eq(8000) 68 | end 69 | 70 | it "unsets the statement timeout after the migration" do 71 | silence_stream($stdout) { run_migration.call } 72 | expect(TimeoutTestHelpers.get(:statement_timeout)).to eq(8001) 73 | end 74 | end 75 | end 76 | 77 | context "when running with transactional DDL" do 78 | let(:run_migration) do 79 | -> { ActiveRecord::Base.transaction { migration.migrate(:up) } } 80 | end 81 | 82 | include_examples "running the migration" 83 | end 84 | 85 | context "when running without transactional DDL" do 86 | let(:run_migration) { -> { migration.migrate(:up) } } 87 | 88 | include_examples "running the migration" 89 | end 90 | end 91 | 92 | describe "the default timeouts" do 93 | before do 94 | $lock_timeout = nil 95 | $statement_timeout = nil 96 | ActiveRecord::SaferMigrations.default_lock_timeout = 6000 97 | ActiveRecord::SaferMigrations.default_statement_timeout = 6001 98 | end 99 | 100 | let(:migration) do 101 | Class.new(migration_base_class) do 102 | def change 103 | $lock_timeout = TimeoutTestHelpers.get(:lock_timeout) 104 | $statement_timeout = TimeoutTestHelpers.get(:statement_timeout) 105 | end 106 | end 107 | end 108 | 109 | it "sets the lock timeout for the duration of the migration" do 110 | silence_stream($stdout) { migration.migrate(:up) } 111 | expect($lock_timeout).to eq(6000) 112 | end 113 | 114 | it "unsets the lock timeout after the migration" do 115 | silence_stream($stdout) { migration.migrate(:up) } 116 | expect(TimeoutTestHelpers.get(:lock_timeout)).to eq(0) 117 | end 118 | 119 | it "sets the statement timeout for the duration of the migration" do 120 | silence_stream($stdout) { migration.migrate(:up) } 121 | expect($statement_timeout).to eq(6001) 122 | end 123 | 124 | it "unsets the statement timeout after the migration" do 125 | silence_stream($stdout) { migration.migrate(:up) } 126 | expect(TimeoutTestHelpers.get(:statement_timeout)).to eq(0) 127 | end 128 | end 129 | 130 | describe "when inheriting from a migration with timeouts defined" do 131 | before do 132 | $lock_timeout = nil 133 | $statement_timeout = nil 134 | ActiveRecord::SaferMigrations.default_lock_timeout = 6000 135 | ActiveRecord::SaferMigrations.default_statement_timeout = 6001 136 | end 137 | 138 | let(:base_migration) do 139 | Class.new(migration_base_class) do 140 | set_lock_timeout(7000) 141 | set_statement_timeout(7001) 142 | def change 143 | $lock_timeout = TimeoutTestHelpers.get(:lock_timeout) 144 | $statement_timeout = TimeoutTestHelpers.get(:statement_timeout) 145 | end 146 | end 147 | end 148 | 149 | context "when the timeout isn't overridden" do 150 | let(:migration) { Class.new(base_migration) {} } 151 | 152 | it "sets the base class' lock timeout for the duration of the migration" do 153 | silence_stream($stdout) { migration.migrate(:up) } 154 | expect($lock_timeout).to eq(7000) 155 | end 156 | 157 | it "sets the base class' statement timeout for the duration of the migration" do 158 | silence_stream($stdout) { migration.migrate(:up) } 159 | expect($statement_timeout).to eq(7001) 160 | end 161 | end 162 | 163 | context "when the timeout is overridden" do 164 | let(:migration) do 165 | Class.new(base_migration) do 166 | set_lock_timeout(8000) 167 | set_statement_timeout(8001) 168 | end 169 | end 170 | 171 | it "sets the subclass' lock timeout for the duration of the migration" do 172 | silence_stream($stdout) { migration.migrate(:up) } 173 | expect($lock_timeout).to eq(8000) 174 | end 175 | 176 | it "sets the subclass' statement timeout for the duration of the migration" do 177 | silence_stream($stdout) { migration.migrate(:up) } 178 | expect($statement_timeout).to eq(8001) 179 | end 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "activerecord-safer_migrations" 5 | 6 | ActiveRecord::SaferMigrations.load 7 | ActiveRecord::Base.establish_connection 8 | 9 | def silence_stream(stream) 10 | old_stream = stream.dup 11 | stream.reopen(IO::NULL) 12 | stream.sync = true 13 | yield 14 | ensure 15 | stream.reopen(old_stream) 16 | old_stream.close 17 | end 18 | 19 | def nuke_migrations 20 | ActiveRecord::Base.connection_pool.with_connection do |conn| 21 | conn.execute("DROP TABLE IF EXISTS schema_migrations") 22 | end 23 | end 24 | 25 | module TimeoutTestHelpers 26 | def self.get(timeout_name) 27 | sql = <<-SQL 28 | SELECT 29 | setting AS #{timeout_name} 30 | FROM 31 | pg_settings 32 | WHERE 33 | name = '#{timeout_name}' 34 | SQL 35 | ActiveRecord::Base.connection.execute(sql).first[timeout_name.to_s].to_i 36 | end 37 | 38 | def self.set(timeout_name, timeout) 39 | ActiveRecord::Base.connection.execute("SET #{timeout_name} = #{timeout}") 40 | end 41 | end 42 | --------------------------------------------------------------------------------