safe-pg-migrations 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: abc17a43f436f429bc2f8983a45b1a61af67a4f8
4
- data.tar.gz: f1c0fdc3261c34fe99218e935e502c639d36f4f1
3
+ metadata.gz: 2a18b54460ec7f3977c9183273705d066793df42
4
+ data.tar.gz: ed2df6a14d6fc3a880b4629f10b6a22d3f6c105e
5
5
  SHA512:
6
- metadata.gz: 3bc4cb7434a1b9059efbc0c69a7a3f73a329b478023db08b0f53d5345ad4346798be592b581e0db0413a574c6c72b2369dcad98402ae4f89a88411525967211d
7
- data.tar.gz: 670b3ef74768301708b85e904defe0566dedfb04762ea85fb2cccf0dccb666a614719430e36e6204e399a7861c59494843750fb6321add67144a49c3fc941846
6
+ metadata.gz: 1d088c3c46622f6fa2f7cc717a8462f06042d3b9cbcd9ae8c0a79fb0f62f2fa1b1b47033050275b25d63add29b4f97795718aeb6dddbc3eae03f66b3614dfa62
7
+ data.tar.gz: d4754e18a023d4e38bfc8c4ae0a387c3e2c38646ba6bcdd776a0673983b885b93456654e27c0793c2841d512f6d40e0102878559b65d6b439dfa713a676b44b9
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Doctolib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ # safe-pg-migrations
2
+
3
+ [![Build Status](https://travis-ci.org/doctolib/safe-pg-migrations.svg?branch=master)](https://travis-ci.org/doctolib/safe-pg-migrations)
4
+
5
+ ## Compatibility
6
+
7
+ Ruby 2.3+
8
+ Rails 5.2+
9
+ PostgreSQL 9.3+
10
+
11
+ ## Running tests
12
+
13
+ ```bash
14
+ bundle
15
+ psql -h localhost -c 'CREATE DATABASE safe_pg_migrations_test'
16
+ rake test
17
+ ```
@@ -1,5 +1,3 @@
1
- class SafePgMigrations
2
- def self.hello
3
- puts 'Hello world!'
4
- end
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'safe-pg-migrations/railtie' if defined?(Rails)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'safe-pg-migrations/configuration'
4
+ require 'safe-pg-migrations/plugins/blocking_activity_logger'
5
+ require 'safe-pg-migrations/plugins/statement_insurer'
6
+ require 'safe-pg-migrations/plugins/statement_retrier'
7
+
8
+ module SafePgMigrations
9
+ PLUGINS = [
10
+ BlockingActivityLogger,
11
+ StatementRetrier,
12
+ StatementInsurer,
13
+ ].freeze
14
+
15
+ class << self
16
+ attr_reader :current_migration
17
+ attr_accessor :enabled
18
+
19
+ def setup_and_teardown(migration, connection)
20
+ @alternate_connection = nil
21
+ @current_migration = migration
22
+ PLUGINS.each { |plugin| connection.extend(plugin) }
23
+ connection.with_setting(:lock_timeout, SafePgMigrations.config.safe_timeout) { yield }
24
+ ensure
25
+ close_alternate_connection
26
+ @current_migration = nil
27
+ end
28
+
29
+ def alternate_connection
30
+ @alternate_connection ||= ActiveRecord::Base.connection_pool.send(:new_connection)
31
+ end
32
+
33
+ def close_alternate_connection
34
+ return unless @alternate_connection
35
+
36
+ @alternate_connection.disconnect!
37
+ @alternate_connection = nil
38
+ end
39
+
40
+ def say(*args)
41
+ return unless current_migration
42
+
43
+ current_migration.say(*args)
44
+ end
45
+
46
+ def say_method_call(method, *args)
47
+ say "#{method}(#{args.map(&:inspect) * ', '})", true
48
+ end
49
+
50
+ def enabled?
51
+ return ENV['SAFE_PG_MIGRATIONS'] == '1' if ENV['SAFE_PG_MIGRATIONS']
52
+ return enabled unless enabled.nil?
53
+ return Rails.env.production? if defined?(Rails)
54
+
55
+ false
56
+ end
57
+
58
+ def config
59
+ @config ||= Configuration.new
60
+ end
61
+ end
62
+
63
+ module Migration
64
+ def exec_migration(connection, direction)
65
+ SafePgMigrations.setup_and_teardown(self, connection) do
66
+ super(connection, direction)
67
+ end
68
+ end
69
+
70
+ def disable_ddl_transaction
71
+ SafePgMigrations.enabled? || super
72
+ end
73
+
74
+ SAFE_METHODS = %i[execute add_column add_index add_reference add_belongs_to change_column_null].freeze
75
+ SAFE_METHODS.each do |method|
76
+ define_method method do |*args|
77
+ return super(*args) unless respond_to?(:safety_assured)
78
+
79
+ safety_assured { super(*args) }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/numeric/time'
4
+
5
+ module SafePgMigrations
6
+ class Configuration
7
+ attr_accessor :safe_timeout
8
+ attr_accessor :blocking_activity_logger_delay # Must be close to but smaller than safe_timeout.
9
+ attr_accessor :batch_size
10
+ attr_accessor :retry_delay
11
+ attr_accessor :max_tries
12
+
13
+ def initialize
14
+ self.safe_timeout = '5s'
15
+ self.blocking_activity_logger_delay = 4.seconds
16
+ self.batch_size = 1000
17
+ self.retry_delay = 2.minutes
18
+ self.max_tries = 5
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module BlockingActivityLogger
5
+ SELECT_BLOCKING_QUERIES_SQL = <<~SQL.squish
6
+ SELECT blocking_activity.query
7
+ FROM pg_catalog.pg_locks blocked_locks
8
+ JOIN pg_catalog.pg_stat_activity blocked_activity
9
+ ON blocked_activity.pid = blocked_locks.pid
10
+ JOIN pg_catalog.pg_locks blocking_locks
11
+ ON blocking_locks.locktype = blocked_locks.locktype
12
+ AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
13
+ AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
14
+ AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
15
+ AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
16
+ AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
17
+ AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
18
+ AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
19
+ AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
20
+ AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
21
+ AND blocking_locks.pid != blocked_locks.pid
22
+ JOIN pg_catalog.pg_stat_activity blocking_activity
23
+ ON blocking_activity.pid = blocking_locks.pid
24
+ WHERE blocked_locks.pid = %d
25
+ SQL
26
+
27
+ %i[
28
+ add_column remove_column add_foreign_key remove_foreign_key change_column_default
29
+ change_column_null create_table add_index remove_index
30
+ ].each do |method|
31
+ define_method method do |*args, &block|
32
+ log_blocking_queries { super(*args, &block) }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def log_blocking_queries
39
+ blocking_queries_retriever_thread =
40
+ Thread.new do
41
+ sleep SafePgMigrations.config.blocking_activity_logger_delay
42
+ SafePgMigrations.alternate_connection.query_values(SELECT_BLOCKING_QUERIES_SQL % raw_connection.backend_pid)
43
+ end
44
+
45
+ yield
46
+
47
+ blocking_queries_retriever_thread.kill
48
+ rescue ActiveRecord::LockWaitTimeout
49
+ SafePgMigrations.say 'Lock timeout.', true
50
+ queries =
51
+ begin
52
+ blocking_queries_retriever_thread.value
53
+ rescue StandardError => e
54
+ SafePgMigrations.say("Error while retrieving blocking queries: #{e}", true)
55
+ nil
56
+ end
57
+
58
+ raise if queries.nil?
59
+
60
+ if queries.empty?
61
+ SafePgMigrations.say 'Could not find any blocking query.', true
62
+ else
63
+ SafePgMigrations.say(
64
+ "Statement was being blocked by the following #{'query'.pluralize(queries.size)}:", true
65
+ )
66
+ SafePgMigrations.say '', true
67
+ queries.each { |query| SafePgMigrations.say " #{query}", true }
68
+ SafePgMigrations.say '', true
69
+ end
70
+
71
+ raise
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module StatementInsurer
5
+ %i[change_column_null add_foreign_key create_table].each do |method|
6
+ define_method method do |*args, &block|
7
+ with_setting(:statement_timeout, SafePgMigrations.config.safe_timeout) { super(*args, &block) }
8
+ end
9
+ end
10
+
11
+ def add_column(table_name, column_name, type, **options)
12
+ default = options.delete(:default)
13
+ null = options.delete(:null)
14
+
15
+ if !default.nil? || null == false
16
+ SafePgMigrations.say_method_call(:add_column, table_name, column_name, type, **options)
17
+ end
18
+
19
+ super
20
+
21
+ unless default.nil?
22
+ SafePgMigrations.say_method_call(:change_column_default, table_name, column_name, default)
23
+ change_column_default(table_name, column_name, default)
24
+
25
+ SafePgMigrations.say_method_call(:backfill_column_default, table_name, column_name)
26
+ backfill_column_default(table_name, column_name)
27
+ end
28
+
29
+ if null == false # rubocop:disable Style/GuardClause
30
+ SafePgMigrations.say_method_call(:change_column_null, table_name, column_name, null)
31
+ change_column_null(table_name, column_name, null)
32
+ end
33
+ end
34
+
35
+ def add_index(table_name, column_name, **options)
36
+ if SafePgMigrations.enabled?
37
+ options[:algorithm] = :concurrently
38
+ SafePgMigrations.say_method_call(:add_index, table_name, column_name, **options)
39
+ end
40
+ without_statement_timeout { super }
41
+ end
42
+
43
+ def remove_index(table_name, options = {})
44
+ options = { column: options } unless options.is_a?(Hash)
45
+ if SafePgMigrations.enabled?
46
+ options[:algorithm] = :concurrently
47
+ SafePgMigrations.say_method_call(:remove_index, table_name, **options)
48
+ end
49
+ without_statement_timeout { super }
50
+ end
51
+
52
+ def backfill_column_default(table_name, column_name)
53
+ quoted_table_name = quote_table_name(table_name)
54
+ quoted_column_name = quote_column_name(column_name)
55
+ primary_key_offset = 0
56
+ loop do
57
+ ids = query_values <<~SQL.squish
58
+ SELECT id FROM #{quoted_table_name} WHERE id > #{primary_key_offset}
59
+ ORDER BY id LIMIT #{SafePgMigrations.config.batch_size}
60
+ SQL
61
+ break if ids.empty?
62
+
63
+ primary_key_offset = ids.last
64
+ execute <<~SQL.squish
65
+ UPDATE #{quoted_table_name} SET #{quoted_column_name} = DEFAULT WHERE id IN (#{ids.join(',')})
66
+ SQL
67
+ end
68
+ end
69
+
70
+ def with_setting(key, value)
71
+ old_value = query_value("SHOW #{key}")
72
+ execute("SET #{key} TO #{quote(value)}")
73
+ begin
74
+ yield
75
+ ensure
76
+ begin
77
+ execute("SET #{key} TO #{quote(old_value)}")
78
+ rescue ActiveRecord::StatementInvalid => e
79
+ # Swallow `PG::InFailedSqlTransaction` exceptions so as to keep the
80
+ # original exception (if any).
81
+ raise unless e.cause.is_a?(PG::InFailedSqlTransaction)
82
+ end
83
+ end
84
+ end
85
+
86
+ def without_statement_timeout
87
+ with_setting(:statement_timeout, 0) { yield }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ module StatementRetrier
5
+ RETRIABLE_SCHEMA_STATEMENTS = %i[
6
+ add_column remove_column add_foreign_key remove_foreign_key change_column_default
7
+ change_column_null
8
+ ].freeze
9
+
10
+ RETRIABLE_SCHEMA_STATEMENTS.each do |method|
11
+ define_method method do |*args, &block|
12
+ retry_if_lock_timeout { super(*args, &block) }
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def retry_if_lock_timeout
19
+ remaining_tries = SafePgMigrations.config.max_tries
20
+ begin
21
+ remaining_tries -= 1
22
+ yield
23
+ rescue ActiveRecord::LockWaitTimeout
24
+ raise if transaction_open? # Retrying is useless if we're inside a transaction.
25
+ raise unless remaining_tries > 0
26
+
27
+ retry_delay = SafePgMigrations.config.retry_delay
28
+ SafePgMigrations.say "Retrying in #{retry_delay} seconds...", true
29
+ sleep retry_delay
30
+ SafePgMigrations.say 'Retrying now.', true
31
+ retry
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'safe-pg-migrations/base'
4
+
5
+ module SafePgMigrations
6
+ class Railtie < Rails::Railtie
7
+ initializer 'sage_pg_migrations.insert_into_active_record' do
8
+ ActiveSupport.on_load :active_record do
9
+ ActiveRecord::Migration.prepend(SafePgMigrations::Migration)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SafePgMigrations
4
+ VERSION = '0.0.1'
5
+ end
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.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu Prat
@@ -9,16 +9,165 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-10-08 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2018-10-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '5.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '5.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: activesupport
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '5.2'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '5.2'
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: mocha
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: pg
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: pry
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: pry-coolline
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rake
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rubocop
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
14
154
  description: Make your PG migrations safe.
15
155
  email: matthieuprat@gmail.com
16
156
  executables: []
17
157
  extensions: []
18
158
  extra_rdoc_files: []
19
159
  files:
160
+ - LICENSE
161
+ - README.md
20
162
  - lib/safe-pg-migrations.rb
21
- homepage: http://rubygems.org/doctolib/safe-pg-migrations
163
+ - lib/safe-pg-migrations/base.rb
164
+ - lib/safe-pg-migrations/configuration.rb
165
+ - lib/safe-pg-migrations/plugins/blocking_activity_logger.rb
166
+ - lib/safe-pg-migrations/plugins/statement_insurer.rb
167
+ - lib/safe-pg-migrations/plugins/statement_retrier.rb
168
+ - lib/safe-pg-migrations/railtie.rb
169
+ - lib/safe-pg-migrations/version.rb
170
+ homepage: https://github.com/doctolib/safe-pg-migrations
22
171
  licenses:
23
172
  - MIT
24
173
  metadata: {}
@@ -30,7 +179,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
30
179
  requirements:
31
180
  - - ">="
32
181
  - !ruby/object:Gem::Version
33
- version: '0'
182
+ version: '2.3'
34
183
  required_rubygems_version: !ruby/object:Gem::Requirement
35
184
  requirements:
36
185
  - - ">="
@@ -38,7 +187,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
38
187
  version: '0'
39
188
  requirements: []
40
189
  rubyforge_project:
41
- rubygems_version: 2.6.13
190
+ rubygems_version: 2.6.14.1
42
191
  signing_key:
43
192
  specification_version: 4
44
193
  summary: Make your PG migrations safe.