safe-pg-migrations 0.0.1 → 0.0.2
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.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e766b867ff3d15f71bdaaa161e595e51bd1b83b9
|
4
|
+
data.tar.gz: b81ced0f60a21fe8e811962a614fc913ad97349a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd45ef3561909949c81b0b9927ed8e3f2b58ab101fd5436f8cb11afb72696e0668bb1bc773c8d66693f07c17456ce9b75f046f20866d1780165f96fd1e17f4a5
|
7
|
+
data.tar.gz: f0fca599eccc2ebb909c168ff9509e6f04ef6b16853aa4ae881a1274775b271f37881fe13c12edc4b03d4c7630d69010e3ef6f2401e861326c9380b8db588ef6
|
data/README.md
CHANGED
@@ -1,17 +1,151 @@
|
|
1
|
-
# safe-pg-migrations
|
1
|
+
# safe-pg-migrations [](https://travis-ci.org/doctolib/safe-pg-migrations)
|
2
2
|
|
3
|
-
|
3
|
+
ActiveRecord migrations for Postgres made safe.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Requirements
|
6
6
|
|
7
|
-
Ruby 2.3+
|
8
|
-
Rails 5.2+
|
9
|
-
PostgreSQL 9.3+
|
7
|
+
- Ruby 2.3+
|
8
|
+
- Rails 5.2+
|
9
|
+
- PostgreSQL 9.3+
|
10
10
|
|
11
|
-
##
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Just drop this line in your Gemfile:
|
14
|
+
|
15
|
+
```rb
|
16
|
+
gem 'safe-pg-migrations'
|
17
|
+
```
|
18
|
+
|
19
|
+
## Example
|
20
|
+
|
21
|
+
Consider the following migration:
|
22
|
+
|
23
|
+
```rb
|
24
|
+
class AddAdminToUsers < ActiveRecord::Migration[5.2]
|
25
|
+
def change
|
26
|
+
add_column :users, :admin, :boolean, default: false, null: false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
If the `users` table is large, running this migration on a live Postgres database will likely cause downtime. **Safe PG Migrations** hooks into Active Record so that the following gets executed instead:
|
32
|
+
|
33
|
+
```rb
|
34
|
+
class AddAdminToUsers < ActiveRecord::Migration[5.2]
|
35
|
+
# Do not wrap the migration in a transaction so that locks are held for a shorter time.
|
36
|
+
disable_ddl_transaction!
|
37
|
+
|
38
|
+
def change
|
39
|
+
# Lower Postgres' lock timeout to avoid statement queueing. Acts like a seatbelt.
|
40
|
+
execute "SET lock_timeout TO '5s'" # The lock_timeout duration is customizable.
|
41
|
+
|
42
|
+
# Add the column without the default value and the not-null constraint.
|
43
|
+
add_column :users, :admin, :boolean
|
44
|
+
|
45
|
+
# Set the column's default value.
|
46
|
+
change_column_default :users, :admin, false
|
47
|
+
|
48
|
+
# Backfill the column in batches.
|
49
|
+
User.in_batches.update_all(admin: false)
|
50
|
+
|
51
|
+
# Add the not-null constraint. Beforehand, set a short statement timeout so that
|
52
|
+
# Postgres does not spend too much time performing the full table scan to verify
|
53
|
+
# the column contains no nulls.
|
54
|
+
execute "SET statement_timeout TO '5s'"
|
55
|
+
change_column_null :users, :admin, false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
Under the hood, **Safe PG Migrations** patches `ActiveRecord::Migration` and extends `ActiveRecord::Base.connection` to make potentially dangerous methods—like `add_column`—safe.
|
61
|
+
|
62
|
+
## Motivation
|
63
|
+
|
64
|
+
Writing a safe migration can be daunting. Numerous articles have been written on the topic and a few gems are trying to address the problem. Even for someone who has a pretty good command of Postgres, remembering all the subtleties of explicit locking is not a piece of cake.
|
65
|
+
|
66
|
+
Active Record means developers don't have to be proficient in SQL to interact with a database. In the same way, **Safe PG Migrations** was created so that developers don't have to understand the ins and outs of Postgres to write a safe migration.
|
67
|
+
|
68
|
+
## Feature set
|
69
|
+
|
70
|
+
### Lock timeout
|
71
|
+
|
72
|
+
Most DDL operations (e.g. adding a column, removing a column or adding a default value to a column) take an `ACCESS EXCLUSIVE` lock on the table they are altering. While these operations wait to acquire their lock, other statements are blocked. Before running a migration, **Safe PG Migrations** sets a short lock timeout so that statements are not blocked for too long.
|
73
|
+
|
74
|
+
See [PostgreSQL Alter Table and Long Transactions](http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html) and [Migrations and Long Transactions](https://www.fin.com/post/2018/1/migrations-and-long-transactions) for detailed explanations of the matter.
|
75
|
+
|
76
|
+
### Statement timeout
|
77
|
+
|
78
|
+
Adding a foreign key or a not-null constraint can take a lot of time on a large table. The problem is that those operations take `ACCESS EXCLUSIVE` locks. We clearly don't want them to hold these locks for too long. Thus, **Safe PG Migrations** runs them with a short statement timeout.
|
79
|
+
|
80
|
+
See [Zero-downtime Postgres migrations - the hard parts](https://gocardless.com/blog/zero-downtime-postgres-migrations-the-hard-parts/) for a detailed explanation on the subject.
|
81
|
+
|
82
|
+
### Prevent wrapping migrations in transaction
|
83
|
+
|
84
|
+
When **Safe PG Migrations** is enabled (which is the case by default if `Rails.env.production?` is true), migrations are not wrapped in a transaction. This is for several reasons:
|
85
|
+
|
86
|
+
- We want to release locks as soon as possible.
|
87
|
+
- In order to be able to retry statements that have failed because of a lock timeout, we have to be outside a transaction.
|
88
|
+
- In order to add an index concurrently, we have to be outside a transaction.
|
89
|
+
|
90
|
+
Note that if a migration fails, it won't be rollbacked. This can result in migrations being partially applied. In that case, they need to be manually reverted.
|
91
|
+
|
92
|
+
### Safe `add_column`
|
93
|
+
|
94
|
+
Adding a column with a default value and a not-null constraint is [dangerous](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).
|
95
|
+
|
96
|
+
**Safe PG Migrations** makes it safe by:
|
97
|
+
|
98
|
+
1. Adding the column without the default value and the not null constraint,
|
99
|
+
2. Then set the default value on the column,
|
100
|
+
3. Then backfilling the column,
|
101
|
+
4. And then adding the not null constraint with a short statement timeout.
|
102
|
+
|
103
|
+
Note: the addition of the not null constraint may timeout. In that case, you may want to add the not-null constraint as initially not valid and validate it in a separate statement. See [Adding a not-null constraint on Postgres with minimal locking](https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c).
|
104
|
+
|
105
|
+
### Concurrent indexes
|
106
|
+
|
107
|
+
Creating an index requires a `SHARE` lock on the target table which blocks all write on the table while the index is created (which can take some time on a large table). This is usually not practical in a live environment. Thus, **Safe PG Migrations** ensures indexes are created concurrently.
|
108
|
+
|
109
|
+
### Retry after lock timeout
|
110
|
+
|
111
|
+
When a statement fails with a lock timeout, **Safe PG Migrations** retries them (5 times max).
|
112
|
+
|
113
|
+
### Blocking activity logging
|
114
|
+
|
115
|
+
If a statement fails with a lock timeout, **Safe PG Migrations** will try to tell you what was the blocking statement.
|
116
|
+
|
117
|
+
## Runnings tests
|
12
118
|
|
13
119
|
```bash
|
14
120
|
bundle
|
15
121
|
psql -h localhost -c 'CREATE DATABASE safe_pg_migrations_test'
|
16
122
|
rake test
|
17
123
|
```
|
124
|
+
|
125
|
+
## Authors
|
126
|
+
|
127
|
+
- [Matthieu Prat](https://github.com/matthieuprat)
|
128
|
+
- [Romain Choquet](https://github.com/rchoquet)
|
129
|
+
|
130
|
+
## License
|
131
|
+
|
132
|
+
[MIT](https://github.com/doctolib/safe-pg-migrations/blob/master/LICENSE) © [Doctolib](https://github.com/doctolib/)
|
133
|
+
|
134
|
+
## Additional resources
|
135
|
+
|
136
|
+
Alternatives:
|
137
|
+
|
138
|
+
- https://github.com/gocardless/activerecord-safer_migrations
|
139
|
+
- https://github.com/ankane/strong_migrations
|
140
|
+
- https://github.com/LendingHome/zero_downtime_migrations
|
141
|
+
|
142
|
+
Interesting reads:
|
143
|
+
|
144
|
+
- https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/
|
145
|
+
- https://www.fin.com/post/2018/1/migrations-and-long-transactions
|
146
|
+
- http://www.joshuakehn.com/2017/9/9/postgresql-alter-table-and-long-transactions.html
|
147
|
+
- https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c
|
148
|
+
- https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/
|
149
|
+
- https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/
|
150
|
+
- https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/
|
151
|
+
- https://blog.codeship.com/rails-migrations-zero-downtime/
|
@@ -4,10 +4,13 @@ require 'safe-pg-migrations/configuration'
|
|
4
4
|
require 'safe-pg-migrations/plugins/blocking_activity_logger'
|
5
5
|
require 'safe-pg-migrations/plugins/statement_insurer'
|
6
6
|
require 'safe-pg-migrations/plugins/statement_retrier'
|
7
|
+
require 'safe-pg-migrations/plugins/idem_potent_statements'
|
7
8
|
|
8
9
|
module SafePgMigrations
|
10
|
+
# Order matters: the bottom-most plugin will have precedence
|
9
11
|
PLUGINS = [
|
10
12
|
BlockingActivityLogger,
|
13
|
+
IdemPotentStatements,
|
11
14
|
StatementRetrier,
|
12
15
|
StatementInsurer,
|
13
16
|
].freeze
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SafePgMigrations
|
4
|
+
module IdemPotentStatements
|
5
|
+
def add_index(table_name, column_name, **options)
|
6
|
+
index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, index_column_names(column_name))
|
7
|
+
return super unless index_name_exists?(table_name, index_name)
|
8
|
+
|
9
|
+
return if index_valid?(index_name)
|
10
|
+
|
11
|
+
remove_index(table_name, name: index_name)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def index_valid?(index_name)
|
18
|
+
query_value <<~SQL.squish
|
19
|
+
SELECT indisvalid
|
20
|
+
FROM pg_index i
|
21
|
+
JOIN pg_class c
|
22
|
+
ON i.indexrelid = c.oid
|
23
|
+
WHERE c.relname = '#{index_name}';
|
24
|
+
SQL
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -4,7 +4,7 @@ module SafePgMigrations
|
|
4
4
|
module StatementRetrier
|
5
5
|
RETRIABLE_SCHEMA_STATEMENTS = %i[
|
6
6
|
add_column remove_column add_foreign_key remove_foreign_key change_column_default
|
7
|
-
change_column_null
|
7
|
+
change_column_null add_index
|
8
8
|
].freeze
|
9
9
|
|
10
10
|
RETRIABLE_SCHEMA_STATEMENTS.each do |method|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: safe-pg-migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthieu Prat
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2018-10-
|
12
|
+
date: 2018-10-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -163,6 +163,7 @@ files:
|
|
163
163
|
- lib/safe-pg-migrations/base.rb
|
164
164
|
- lib/safe-pg-migrations/configuration.rb
|
165
165
|
- lib/safe-pg-migrations/plugins/blocking_activity_logger.rb
|
166
|
+
- lib/safe-pg-migrations/plugins/idem_potent_statements.rb
|
166
167
|
- lib/safe-pg-migrations/plugins/statement_insurer.rb
|
167
168
|
- lib/safe-pg-migrations/plugins/statement_retrier.rb
|
168
169
|
- lib/safe-pg-migrations/railtie.rb
|