activerecord-safer_migrations 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +30 -0
- data/.ruby-version +1 -0
- data/.travis.yml +17 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +73 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/activerecord-safer_migrations.gemspec +17 -0
- data/lib/active_record/safer_migrations/migration.rb +64 -0
- data/lib/active_record/safer_migrations/postgresql_adapter.rb +38 -0
- data/lib/active_record/safer_migrations/railtie.rb +11 -0
- data/lib/active_record/safer_migrations/setting_helper.rb +49 -0
- data/lib/active_record/safer_migrations/version.rb +5 -0
- data/lib/activerecord-safer_migrations.rb +38 -0
- data/spec/active_record/safer_migrations/migration_spec.rb +160 -0
- data/spec/spec_helper.rb +39 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 75845456397be277043e0b89d6fedbac416d3fe3
|
4
|
+
data.tar.gz: 01add6f99123fd1bd0246f41504ec754d674dc87
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b95179e85acb6a179fe9a05f018a4775fad27680bdb8f5945dbe43cc12187a53d91b13e4567097721df8ad5f14cdd465d68fb6aedc0896d86e19214e5b932d24
|
7
|
+
data.tar.gz: dbcd63bb99f1202a72d7a01b7857af9635bcce72d1c9703bedfa3035b607b9fcf2752bcb9953bba2b54412b0b46702f93a35c79355b7f78edf1d5729cd035148
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/.bundle
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisplayCopNames: true
|
3
|
+
|
4
|
+
# Use trailing rather than leading dots on multi-line call chains
|
5
|
+
Style/DotPosition:
|
6
|
+
EnforcedStyle: trailing
|
7
|
+
|
8
|
+
Style/Documentation:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Style/StringLiterals:
|
12
|
+
EnforcedStyle: double_quotes
|
13
|
+
|
14
|
+
Style/TrailingComma:
|
15
|
+
EnforcedStyleForMultiline: comma
|
16
|
+
|
17
|
+
Style/AccessorMethodName:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/SignalException:
|
21
|
+
EnforcedStyle: only_raise
|
22
|
+
|
23
|
+
Style/GlobalVars:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Style/FileName:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Metrics/LineLength:
|
30
|
+
Max: 100
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0-p353
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
rvm:
|
4
|
+
- 2.2
|
5
|
+
|
6
|
+
addons:
|
7
|
+
postgresql: "9.3"
|
8
|
+
|
9
|
+
before_script:
|
10
|
+
- psql -c 'CREATE DATABASE safer_migrations_test;' -U postgres
|
11
|
+
|
12
|
+
script:
|
13
|
+
- bundle exec rspec spec
|
14
|
+
- bundle exec rubocop
|
15
|
+
|
16
|
+
env:
|
17
|
+
- "DATABASE_URL=postgres://postgres@localhost/safer_migrations_test"
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
activerecord-safer_migrations (0.1.0)
|
5
|
+
activerecord (~> 4.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (4.2.5)
|
11
|
+
activesupport (= 4.2.5)
|
12
|
+
builder (~> 3.1)
|
13
|
+
activerecord (4.2.5)
|
14
|
+
activemodel (= 4.2.5)
|
15
|
+
activesupport (= 4.2.5)
|
16
|
+
arel (~> 6.0)
|
17
|
+
activesupport (4.2.5)
|
18
|
+
i18n (~> 0.7)
|
19
|
+
json (~> 1.7, >= 1.7.7)
|
20
|
+
minitest (~> 5.1)
|
21
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
arel (6.0.3)
|
24
|
+
ast (2.1.0)
|
25
|
+
astrolabe (1.3.1)
|
26
|
+
parser (~> 2.2)
|
27
|
+
builder (3.2.2)
|
28
|
+
diff-lcs (1.2.5)
|
29
|
+
i18n (0.7.0)
|
30
|
+
json (1.8.3)
|
31
|
+
minitest (5.8.3)
|
32
|
+
parser (2.2.3.0)
|
33
|
+
ast (>= 1.1, < 3.0)
|
34
|
+
pg (0.18.3)
|
35
|
+
powerpack (0.1.1)
|
36
|
+
rainbow (2.0.0)
|
37
|
+
rspec (3.3.0)
|
38
|
+
rspec-core (~> 3.3.0)
|
39
|
+
rspec-expectations (~> 3.3.0)
|
40
|
+
rspec-mocks (~> 3.3.0)
|
41
|
+
rspec-core (3.3.2)
|
42
|
+
rspec-support (~> 3.3.0)
|
43
|
+
rspec-expectations (3.3.1)
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
45
|
+
rspec-support (~> 3.3.0)
|
46
|
+
rspec-mocks (3.3.2)
|
47
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
48
|
+
rspec-support (~> 3.3.0)
|
49
|
+
rspec-support (3.3.0)
|
50
|
+
rubocop (0.35.1)
|
51
|
+
astrolabe (~> 1.3)
|
52
|
+
parser (>= 2.2.3.0, < 3.0)
|
53
|
+
powerpack (~> 0.1)
|
54
|
+
rainbow (>= 1.99.1, < 3.0)
|
55
|
+
ruby-progressbar (~> 1.7)
|
56
|
+
tins (<= 1.6.0)
|
57
|
+
ruby-progressbar (1.7.5)
|
58
|
+
thread_safe (0.3.5)
|
59
|
+
tins (1.6.0)
|
60
|
+
tzinfo (1.2.2)
|
61
|
+
thread_safe (~> 0.1)
|
62
|
+
|
63
|
+
PLATFORMS
|
64
|
+
ruby
|
65
|
+
|
66
|
+
DEPENDENCIES
|
67
|
+
activerecord-safer_migrations!
|
68
|
+
pg (~> 0.18.3)
|
69
|
+
rspec (~> 3.3.0)
|
70
|
+
rubocop (~> 0.35.1)
|
71
|
+
|
72
|
+
BUNDLED WITH
|
73
|
+
1.10.6
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
## ActiveRecord safer migration helpers
|
2
|
+
|
3
|
+
*Note: this library only supports PostgreSQL 9.3+. If you're interested in adding support for other databases, we're open to pull requests!*
|
4
|
+
|
5
|
+
Postgres holds ACCESS EXCLUSIVE locks for [almost all][pg-alter-table] DDL
|
6
|
+
operations. ACCESS EXCLUSIVE locks conflict with all other table-level locks,
|
7
|
+
which can cause issues in several situations. For instance:
|
8
|
+
|
9
|
+
1. If the lock is held for a long time, all other access to the table will be
|
10
|
+
blocked, which can result in downtime.
|
11
|
+
2. Even if the lock is only held breifly, it will block all other access to the
|
12
|
+
table while it is in the lock queue, as it conflicts with all other locks.
|
13
|
+
The lock can't be acquired until all other queries ahead of it have finished,
|
14
|
+
so having to wait on long-running queries can also result in downtime.
|
15
|
+
See [here][blog-post] for more details.
|
16
|
+
|
17
|
+
Both these issues can be avoided by setting timeouts on the migration connection -
|
18
|
+
`statement_timeout` and `lock_timeout` respectively.
|
19
|
+
|
20
|
+
Once this gem is loaded, all migrations will automatically have a
|
21
|
+
`lock_timeout` and a `statement_timeout` set. The initial `lock_timeout`
|
22
|
+
default is 750ms, and the initial `statement_timeout` default is 1500ms. Both
|
23
|
+
defaults can be easily changed (e.g. in a Rails initializer).
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
ActiveRecord::SaferMigrations.default_lock_timeout = 1000
|
27
|
+
ActiveRecord::SaferMigrations.default_statement_timeout = 2000
|
28
|
+
```
|
29
|
+
|
30
|
+
To explicitly set timeouts for a given migration, use the `set_lock_timeout` and
|
31
|
+
`set_statement_timeout` class methods in the migration.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class LockTest < ActiveRecord::Migration
|
35
|
+
set_lock_timeout(250)
|
36
|
+
set_statement_timeout(750)
|
37
|
+
|
38
|
+
def change
|
39
|
+
create_table :lock_test
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
To disable timeouts for a migration, use the `disable_lock_timeout!` and
|
45
|
+
`disable_statement_timeout!` class methods. Note that this is [extremely
|
46
|
+
dangerous][blog-post] if you're doing any schema alterations in your migration.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
class LockTest < ActiveRecord::Migration
|
50
|
+
# Only do this if you really know what you're doing!
|
51
|
+
disable_lock_timeout!
|
52
|
+
disable_statement_timeout!
|
53
|
+
|
54
|
+
def change
|
55
|
+
create_table :lock_test
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
### Use with PgBouncer
|
61
|
+
|
62
|
+
This gem sets session-level settings on Postgres connections. If you're using
|
63
|
+
PgBouncer in transaction pooling mode, using session-level settings is
|
64
|
+
dangerous, as you can't guarantee which connection will receive the setting.
|
65
|
+
For this reason, this gem is incompatible with transaction-pooling and should
|
66
|
+
only be used if migrations are run on connections that support session-level
|
67
|
+
features.
|
68
|
+
|
69
|
+
[blog-post]: https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/
|
70
|
+
[pg-alter-table]: http://www.postgresql.org/docs/9.4/static/sql-altertable.html
|
71
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path("../lib/active_record/safer_migrations/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.name = "activerecord-safer_migrations"
|
5
|
+
gem.version = ActiveRecord::SaferMigrations::VERSION
|
6
|
+
gem.date = "2015-08-10"
|
7
|
+
gem.summary = "ActiveRecord migration helpers to avoid downtime"
|
8
|
+
gem.description = ""
|
9
|
+
gem.authors = ["GoCardless Engineering"]
|
10
|
+
gem.email = "developers@gocardless.com"
|
11
|
+
gem.files = `git ls-files`.split("\n")
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.homepage = "https://github.com/gocardless/activerecord-safer_migrations"
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.add_runtime_dependency "activerecord", "~> 4.2"
|
17
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "active_record/safer_migrations/setting_helper"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module SaferMigrations
|
5
|
+
module Migration
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
# Use Rails' class_attribute to get an attribute that you can
|
9
|
+
# override in subclasses
|
10
|
+
class_attribute :lock_timeout
|
11
|
+
class_attribute :statement_timeout
|
12
|
+
|
13
|
+
prepend(InstanceMethods)
|
14
|
+
extend(ClassMethods)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
def exec_migration(conn, direction)
|
20
|
+
# lock_timeout is an instance accessor created by class_attribute
|
21
|
+
lock_timeout_ms = lock_timeout || SaferMigrations.default_lock_timeout
|
22
|
+
statement_timeout_ms = statement_timeout || SaferMigrations.default_statement_timeout
|
23
|
+
SettingHelper.new(conn, :lock_timeout, lock_timeout_ms).with_setting do
|
24
|
+
SettingHelper.new(conn, :statement_timeout, statement_timeout_ms).with_setting do
|
25
|
+
super(conn, direction)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def set_lock_timeout(timeout)
|
33
|
+
if timeout == 0
|
34
|
+
raise "Setting lock_timeout to 0 is dangerous - it disables the lock " \
|
35
|
+
"timeout rather than instantly timing out. If you *actually* " \
|
36
|
+
"want to disable the lock timeout (not recommended!), use the " \
|
37
|
+
"`disable_lock_timeout!` method."
|
38
|
+
end
|
39
|
+
self.lock_timeout = timeout
|
40
|
+
end
|
41
|
+
|
42
|
+
def disable_lock_timeout!
|
43
|
+
say "WARNING: disabling the lock timeout. This is very dangerous."
|
44
|
+
self.lock_timeout = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_statement_timeout(timeout)
|
48
|
+
if timeout == 0
|
49
|
+
raise "Setting statement_timeout to 0 is dangerous - it disables the statement " \
|
50
|
+
"timeout rather than instantly timing out. If you *actually* " \
|
51
|
+
"want to disable the statement timeout (not recommended!), use the " \
|
52
|
+
"`disable_statement_timeout!` method."
|
53
|
+
end
|
54
|
+
self.statement_timeout = timeout
|
55
|
+
end
|
56
|
+
|
57
|
+
def disable_statement_timeout!
|
58
|
+
say "WARNING: disabling the statement timeout. This is very dangerous."
|
59
|
+
self.statement_timeout = 0
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module SaferMigrations
|
3
|
+
module PostgreSQLAdapter
|
4
|
+
SET_SETTING_SQL = <<-SQL
|
5
|
+
UPDATE
|
6
|
+
pg_settings
|
7
|
+
SET
|
8
|
+
setting = :value
|
9
|
+
WHERE
|
10
|
+
name = :setting_name
|
11
|
+
SQL
|
12
|
+
|
13
|
+
GET_SETTING_SQL = <<-SQL
|
14
|
+
SELECT
|
15
|
+
setting
|
16
|
+
FROM
|
17
|
+
pg_settings
|
18
|
+
WHERE
|
19
|
+
name = :setting_name
|
20
|
+
SQL
|
21
|
+
|
22
|
+
def set_setting(setting_name, value)
|
23
|
+
sql = fill_sql_values(SET_SETTING_SQL, value: value, setting_name: setting_name)
|
24
|
+
execute(sql)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_setting(setting_name)
|
28
|
+
sql = fill_sql_values(GET_SETTING_SQL, setting_name: setting_name)
|
29
|
+
result = execute(sql)
|
30
|
+
result.first["setting"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def fill_sql_values(sql, values)
|
34
|
+
ActiveRecord::Base.send(:replace_named_bind_variables, sql, values)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module SaferMigrations
|
3
|
+
class SettingHelper
|
4
|
+
def initialize(connection, setting_name, value)
|
5
|
+
@connection = connection
|
6
|
+
@setting_name = setting_name
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
# We're changing a connection level setting, and we need to make sure we return
|
11
|
+
# it to the original value. It is automatically reverted if set within a
|
12
|
+
# transaction which rolls back, so that case needs handling differently.
|
13
|
+
#
|
14
|
+
# | In Transaction | Not in transaction
|
15
|
+
# ---------------------------------------------------------
|
16
|
+
# Raises | Reset setting | Reset setting
|
17
|
+
# Doesn't raise | Don't reset setting | Reset setting
|
18
|
+
def with_setting
|
19
|
+
record_current_setting
|
20
|
+
set_new_setting
|
21
|
+
yield
|
22
|
+
reset_setting
|
23
|
+
rescue
|
24
|
+
reset_setting unless in_transaction?
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def record_current_setting
|
31
|
+
@original_value = @connection.get_setting(@setting_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_new_setting
|
35
|
+
puts "-- set_setting(#{@setting_name.inspect}, #{@value})"
|
36
|
+
@connection.set_setting(@setting_name, @value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset_setting
|
40
|
+
puts "-- set_setting(#{@setting_name.inspect}, #{@original_value})"
|
41
|
+
@connection.set_setting(@setting_name, @original_value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def in_transaction?
|
45
|
+
ActiveRecord::Base.connection.open_transactions > 0
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
2
|
+
require "active_record/safer_migrations/postgresql_adapter"
|
3
|
+
require "active_record/safer_migrations/migration"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module SaferMigrations
|
7
|
+
@default_lock_timeout = 750
|
8
|
+
@default_statement_timeout = 1500
|
9
|
+
|
10
|
+
def self.default_lock_timeout
|
11
|
+
@default_lock_timeout
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.default_lock_timeout=(timeout_ms)
|
15
|
+
@default_lock_timeout = timeout_ms
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.default_statement_timeout
|
19
|
+
@default_statement_timeout
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.default_statement_timeout=(timeout_ms)
|
23
|
+
@default_statement_timeout = timeout_ms
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.load
|
27
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
|
28
|
+
include ActiveRecord::SaferMigrations::PostgreSQLAdapter
|
29
|
+
end
|
30
|
+
|
31
|
+
ActiveRecord::Migration.class_eval do
|
32
|
+
include ActiveRecord::SaferMigrations::Migration
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require "active_record/safer_migrations/railtie" if defined?(::Rails)
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe ActiveRecord::SaferMigrations::Migration do
|
4
|
+
before { nuke_migrations }
|
5
|
+
before { TimeoutTestHelpers.set(:lock_timeout, 0) }
|
6
|
+
before { TimeoutTestHelpers.set(:statement_timeout, 0) }
|
7
|
+
|
8
|
+
describe "setting timeouts explicitly" do
|
9
|
+
before { $lock_timeout = nil }
|
10
|
+
before { $statement_timeout = nil }
|
11
|
+
|
12
|
+
shared_examples_for "running the migration" do
|
13
|
+
let(:migration) do
|
14
|
+
Class.new(ActiveRecord::Migration) do
|
15
|
+
set_lock_timeout(5000)
|
16
|
+
set_statement_timeout(5001)
|
17
|
+
|
18
|
+
def change
|
19
|
+
$lock_timeout = TimeoutTestHelpers.get(:lock_timeout)
|
20
|
+
$statement_timeout = TimeoutTestHelpers.get(:statement_timeout)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "sets the lock timeout for the duration of the migration" do
|
26
|
+
silence_stream($stdout) { run_migration.call }
|
27
|
+
expect($lock_timeout).to eq(5000)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "unsets the lock timeout after the migration" do
|
31
|
+
silence_stream($stdout) { run_migration.call }
|
32
|
+
expect(TimeoutTestHelpers.get(:lock_timeout)).to eq(0)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "sets the statement timeout for the duration of the migration" do
|
36
|
+
silence_stream($stdout) { run_migration.call }
|
37
|
+
expect($statement_timeout).to eq(5001)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "unsets the statement timeout after the migration" do
|
41
|
+
silence_stream($stdout) { run_migration.call }
|
42
|
+
expect(TimeoutTestHelpers.get(:statement_timeout)).to eq(0)
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when the original timeout is not 0" do
|
46
|
+
before { TimeoutTestHelpers.set(:lock_timeout, 8000) }
|
47
|
+
before { TimeoutTestHelpers.set(:statement_timeout, 8001) }
|
48
|
+
|
49
|
+
it "unsets the lock timeout after the migration" do
|
50
|
+
silence_stream($stdout) { run_migration.call }
|
51
|
+
expect(TimeoutTestHelpers.get(:lock_timeout)).to eq(8000)
|
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(8001)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when running with transactional DDL" do
|
62
|
+
let(:run_migration) do
|
63
|
+
-> { ActiveRecord::Base.transaction { migration.migrate(:up) } }
|
64
|
+
end
|
65
|
+
|
66
|
+
include_examples "running the migration"
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when running without transactional DDL" do
|
70
|
+
let(:run_migration) { -> { migration.migrate(:up) } }
|
71
|
+
|
72
|
+
include_examples "running the migration"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "the default timeouts" do
|
77
|
+
before { $lock_timeout = nil }
|
78
|
+
before { $statement_timeout = nil }
|
79
|
+
before { ActiveRecord::SaferMigrations.default_lock_timeout = 6000 }
|
80
|
+
before { ActiveRecord::SaferMigrations.default_statement_timeout = 6001 }
|
81
|
+
let(:migration) do
|
82
|
+
Class.new(ActiveRecord::Migration) do
|
83
|
+
def change
|
84
|
+
$lock_timeout = TimeoutTestHelpers.get(:lock_timeout)
|
85
|
+
$statement_timeout = TimeoutTestHelpers.get(:statement_timeout)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it "sets the lock timeout for the duration of the migration" do
|
91
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
92
|
+
expect($lock_timeout).to eq(6000)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "unsets the lock timeout after the migration" do
|
96
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
97
|
+
expect(TimeoutTestHelpers.get(:lock_timeout)).to eq(0)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "sets the statement timeout for the duration of the migration" do
|
101
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
102
|
+
expect($statement_timeout).to eq(6001)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "unsets the statement timeout after the migration" do
|
106
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
107
|
+
expect(TimeoutTestHelpers.get(:statement_timeout)).to eq(0)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "when inheriting from a migration with timeouts defined" do
|
112
|
+
before { $lock_timeout = nil }
|
113
|
+
before { $statement_timeout = nil }
|
114
|
+
before { ActiveRecord::SaferMigrations.default_lock_timeout = 6000 }
|
115
|
+
before { ActiveRecord::SaferMigrations.default_statement_timeout = 6001 }
|
116
|
+
let(:base_migration) do
|
117
|
+
Class.new(ActiveRecord::Migration) do
|
118
|
+
set_lock_timeout(7000)
|
119
|
+
set_statement_timeout(7001)
|
120
|
+
def change
|
121
|
+
$lock_timeout = TimeoutTestHelpers.get(:lock_timeout)
|
122
|
+
$statement_timeout = TimeoutTestHelpers.get(:statement_timeout)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "when the timeout isn't overridden" do
|
128
|
+
let(:migration) { Class.new(base_migration) {} }
|
129
|
+
|
130
|
+
it "sets the base class' lock timeout for the duration of the migration" do
|
131
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
132
|
+
expect($lock_timeout).to eq(7000)
|
133
|
+
end
|
134
|
+
|
135
|
+
it "sets the base class' statement timeout for the duration of the migration" do
|
136
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
137
|
+
expect($statement_timeout).to eq(7001)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "when the timeout is overridden" do
|
142
|
+
let(:migration) do
|
143
|
+
Class.new(base_migration) do
|
144
|
+
set_lock_timeout(8000)
|
145
|
+
set_statement_timeout(8001)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it "sets the subclass' lock timeout for the duration of the migration" do
|
150
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
151
|
+
expect($lock_timeout).to eq(8000)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "sets the subclass' statement timeout for the duration of the migration" do
|
155
|
+
silence_stream($stdout) { migration.migrate(:up) }
|
156
|
+
expect($statement_timeout).to eq(8001)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "active_record"
|
2
|
+
require "activerecord-safer_migrations"
|
3
|
+
|
4
|
+
ActiveRecord::SaferMigrations.load
|
5
|
+
ActiveRecord::Base.establish_connection
|
6
|
+
|
7
|
+
def silence_stream(stream)
|
8
|
+
old_stream = stream.dup
|
9
|
+
stream.reopen(IO::NULL)
|
10
|
+
stream.sync = true
|
11
|
+
yield
|
12
|
+
ensure
|
13
|
+
stream.reopen(old_stream)
|
14
|
+
old_stream.close
|
15
|
+
end
|
16
|
+
|
17
|
+
def nuke_migrations
|
18
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
19
|
+
conn.execute("DROP TABLE IF EXISTS schema_migrations")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module TimeoutTestHelpers
|
24
|
+
def self.get(timeout_name)
|
25
|
+
sql = <<-SQL
|
26
|
+
SELECT
|
27
|
+
setting AS #{timeout_name}
|
28
|
+
FROM
|
29
|
+
pg_settings
|
30
|
+
WHERE
|
31
|
+
name = '#{timeout_name}'
|
32
|
+
SQL
|
33
|
+
ActiveRecord::Base.connection.execute(sql).first[timeout_name.to_s].to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.set(timeout_name, timeout)
|
37
|
+
ActiveRecord::Base.connection.execute("SET #{timeout_name} = #{timeout}")
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-safer_migrations
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- GoCardless Engineering
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.2'
|
27
|
+
description: ''
|
28
|
+
email: developers@gocardless.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- .gitignore
|
34
|
+
- .rubocop.yml
|
35
|
+
- .ruby-version
|
36
|
+
- .travis.yml
|
37
|
+
- Gemfile
|
38
|
+
- Gemfile.lock
|
39
|
+
- LICENSE.txt
|
40
|
+
- README.md
|
41
|
+
- activerecord-safer_migrations.gemspec
|
42
|
+
- lib/active_record/safer_migrations/migration.rb
|
43
|
+
- lib/active_record/safer_migrations/postgresql_adapter.rb
|
44
|
+
- lib/active_record/safer_migrations/railtie.rb
|
45
|
+
- lib/active_record/safer_migrations/setting_helper.rb
|
46
|
+
- lib/active_record/safer_migrations/version.rb
|
47
|
+
- lib/activerecord-safer_migrations.rb
|
48
|
+
- spec/active_record/safer_migrations/migration_spec.rb
|
49
|
+
- spec/spec_helper.rb
|
50
|
+
homepage: https://github.com/gocardless/activerecord-safer_migrations
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.0.14
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: ActiveRecord migration helpers to avoid downtime
|
74
|
+
test_files: []
|