pg_ha_migrations 0.1.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 764006f9563011eaf0c11d51dfdac477b273f6e63687df9d9448861129640615
4
+ data.tar.gz: 2ddde5f6e1e39ab07034ff6d43bdd5a9c5f0b71ab75b6c9a5100f4ebfd3c2bec
5
+ SHA512:
6
+ metadata.gz: 993b63e38c85ae14660d300e122c3ad6d748e88d12a62182624e1b80b18862a61fe337c88ecfca7e0e149f64b575d08ec09248972e3dc44fab844d7bc03a5954
7
+ data.tar.gz: 2bdccc2af3fa5e6834a31761c7e19ef4dc1983f5a84dc2b7f7dd8c188ac3c6a2e5ff745de1907bc2ff66c92976668f070eedabb40e08acb5bfe6d02617f8c6eb
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.3
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.3
5
+ before_install: gem install bundler -v 1.15.4
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at code@getbraintree.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in pg_ha_migrations.gemspec
6
+ gemspec
7
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Braintree Payments
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # PgHaMigrations
2
+
3
+ PgHaMigrations is a gem that applies learned best practices to ActiveRecord Migrations.
4
+
5
+ Provided functionality:
6
+ - [Migrations](#migrations)
7
+ - [Utilities](#utilities)
8
+ - [Rake Tasks](#rake-tasks)
9
+
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'pg_ha_migrations'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install pg_ha_migrations
26
+
27
+ ## Usage
28
+
29
+ ### Migrations
30
+
31
+ In general, existing migrations are prefixed with `unsafe_` and safer alternatives are provided prefixed with `safe_`.
32
+
33
+ Migrations prefixed with `unsafe_` will warn when invoked. The API is designed to be explicit yet remain flexible. There may be situations where invoking the unsafe migration is preferred.
34
+
35
+ Migrations prefixed with `safe_` prefer concurrent operations where available, set low lock timeouts where appropriate, and decompose operations into multiple safe steps.
36
+
37
+ The following functionality is currently unsupported:
38
+
39
+ - Rollbacks
40
+ - Generators
41
+ - schema.rb
42
+
43
+ #### safe\_create\_table
44
+
45
+ Safely creates a new table.
46
+
47
+ ```ruby
48
+ safe_create_table :table do |t|
49
+ t.type :column
50
+ end
51
+ ```
52
+
53
+ #### safe\_create\_enum\_type
54
+
55
+ Safely create a new enum without values.
56
+
57
+ ```ruby
58
+ safe_create_enum_type :enum
59
+ ```
60
+ Or, safely create the enum with values.
61
+ ```ruby
62
+ safe_create_enum_type :enum, ["value1", "value2"]
63
+ ```
64
+
65
+ #### safe\_add\_enum\_value
66
+
67
+ Safely add a new enum value.
68
+
69
+ ```ruby
70
+ safe_add_enum_value :enum, "value"
71
+ ```
72
+
73
+ #### safe\_add\_column
74
+
75
+ Safely add a column.
76
+
77
+ ```ruby
78
+ safe_add_column :table, :column, :type
79
+ ```
80
+
81
+ #### unsafe\_add\_column
82
+
83
+ Unsafely add a column, but do so with a lock that is safely acquired.
84
+
85
+ ```ruby
86
+ unsafe_add_column :table, :column, :type
87
+ ```
88
+
89
+ #### safe\_change\_column\_default
90
+
91
+ Safely change the default value for a column.
92
+
93
+ ```ruby
94
+ safe_change_column_default :table, :column, "value"
95
+ ```
96
+
97
+ #### safe\_make\_column\_nullable
98
+
99
+ Safely make the column nullable.
100
+
101
+ ```ruby
102
+ safe_make_column_nullable :table, :column
103
+ ```
104
+
105
+ #### unsafe\_make\_column\_not\_nullable
106
+
107
+ Unsafely make a column not nullable.
108
+
109
+ ```ruby
110
+ unsafe_make_column_not_nullable :table, :column
111
+ ```
112
+
113
+ #### safe\_add\_concurrent\_index
114
+
115
+ Add an index concurrently. Migrations that contain this statement must also include `disable_ddl_transaction!`.
116
+
117
+ ```ruby
118
+ safe_add_concurrent_index :table, :column
119
+ ```
120
+
121
+ Add a composite btree index.
122
+
123
+ ```ruby
124
+ safe_add_concurrent_index :table, [:column1, :column2], name: "index_name", using: :btree
125
+ ```
126
+
127
+ #### safe\_remove\_concurrent\_index
128
+
129
+ Safely remove an index. Migrations that contain this statement must also include `disable_ddl_transaction!`.
130
+
131
+ ```ruby
132
+ safe_remove_concurrent_index :table, :name => :index_name
133
+ ```
134
+
135
+
136
+ ### Utilities
137
+
138
+ #### safely\_acquire\_lock\_for\_table
139
+
140
+ Safely acquire a lock for a table.
141
+
142
+ ```ruby
143
+ safely_acquire_lock_for_table(:table) do
144
+ ...
145
+ end
146
+ ```
147
+
148
+ #### adjust\_lock\_timeout
149
+
150
+ Adjust lock timeout.
151
+
152
+ ```ruby
153
+ adjust_lock_timeout(seconds) do
154
+ ...
155
+ end
156
+ ```
157
+
158
+ #### adjust\_statement\_timeout
159
+
160
+ Adjust statement timeout.
161
+
162
+ ```ruby
163
+ adjust_statement_timeout(seconds) do
164
+ ...
165
+ end
166
+ ```
167
+
168
+ #### safe\_set\_maintenance\_work\_mem\_gb
169
+
170
+ Set maintenance work mem.
171
+
172
+ ```ruby
173
+ safe_set_maintenance_work_mem_gb 1
174
+ ```
175
+
176
+
177
+ ### Rake Tasks
178
+
179
+ Use this to check for blocking transactions before migrating.
180
+
181
+ $ bundle exec rake pg_ha_migrations:check_blocking_database_transactions
182
+
183
+ This rake task expects that you already have a connection open to your database. We suggest that you add another rake task to open the connection and then add that as a prerequisite for `pg_ha_migrations:check_blocking_database_transactions`.
184
+
185
+ ```ruby
186
+ namespace :db do
187
+ desc "Establish a database connection"
188
+ task :establish_connection do
189
+ ActiveRecord::Base.establish_connection
190
+ end
191
+ end
192
+
193
+ Rake::Task["pg_ha_migrations:check_blocking_database_transactions"].enhance ["db:establish_connection"]
194
+ ```
195
+
196
+
197
+ ## Development
198
+
199
+ After checking out the repo, run `bin/setup` to install dependencies and start a postgres docker container. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
200
+
201
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
202
+
203
+ ## Contributing
204
+
205
+ Bug reports and pull requests are welcome on GitHub at https://github.com/braintreeps/pg_ha_migrations. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
206
+
207
+ ## License
208
+
209
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
210
+
211
+ ## Code of Conduct
212
+
213
+ Everyone interacting in the PgHaMigrations project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/braintreeps/pg_ha_migrations/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require_relative File.join("lib", "pg_ha_migrations")
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pg_ha_migrations"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
9
+
10
+ # Launch a blank postgres image for testing
11
+ docker run -d -p 127.0.0.1:5432:5432 postgres:10
@@ -0,0 +1,32 @@
1
+ require "pg_ha_migrations/version"
2
+ require "rails"
3
+ require "active_record"
4
+ require "active_record/migration"
5
+ require "relation_to_struct"
6
+
7
+ module PgHaMigrations
8
+ LOCK_TIMEOUT_SECONDS = 5
9
+ LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER = 5
10
+
11
+ # Safe versus unsafe in this context specifically means the following:
12
+ # - Safe operations will not block for long periods of time.
13
+ # - Unsafe operations _may_ block for long periods of time.
14
+ UnsafeMigrationError = Class.new(Exception)
15
+
16
+ # Invalid migrations are operations which we expect to put the schema
17
+ # into a state inconsistent with our current guidelines; e.g., using
18
+ # a 32-bit foreign key value when the referenced primary key column
19
+ # is a 64-bit value.
20
+ InvalidMigrationError = Class.new(Exception)
21
+
22
+ # This gem only supports the PostgreSQL adapter at this time.
23
+ UnsupportedAdapter = Class.new(Exception)
24
+ end
25
+
26
+ require "pg_ha_migrations/blocking_database_transactions"
27
+ require "pg_ha_migrations/blocking_database_transactions_reporter"
28
+ require "pg_ha_migrations/unsafe_statements"
29
+ require "pg_ha_migrations/safe_statements"
30
+ require "pg_ha_migrations/allowed_versions"
31
+ require "pg_ha_migrations/railtie"
32
+
@@ -0,0 +1,23 @@
1
+ require "active_record/migration/compatibility"
2
+
3
+ module PgHaMigrations::AllowedVersions
4
+ ALLOWED_VERSIONS = [4.2, 5.0, 5.1].map do |v|
5
+ begin
6
+ ActiveRecord::Migration[v]
7
+ rescue ArgumentError
8
+ nil
9
+ end
10
+ end.compact
11
+
12
+ def inherited(subclass)
13
+ super
14
+ unless ALLOWED_VERSIONS.include?(subclass.superclass)
15
+ raise StandardError, "#{subclass.superclass} is not a permitted migration class\n" \
16
+ "\n" \
17
+ "To add a new version update the ALLOWED_VERSIONS constant in #{__FILE__}\n" \
18
+ "Currently allowed versions: #{ALLOWED_VERSIONS.map { |v| "ActiveRecord::Migration[#{v.current_version}]" }.join(', ')}"
19
+ end
20
+ end
21
+ end
22
+
23
+ ActiveRecord::Migration.singleton_class.send(:prepend, PgHaMigrations::AllowedVersions)
@@ -0,0 +1,54 @@
1
+ module PgHaMigrations
2
+ class BlockingDatabaseTransactions
3
+ LongRunningTransaction = Struct.new(:database, :current_query, :transaction_age, :tables_with_locks) do
4
+ def description
5
+ "#{database} | tables (#{tables_with_locks.join(', ')}) have been locked for #{transaction_age} | query: #{current_query}"
6
+ end
7
+
8
+ def concurrent_index_creation?
9
+ !!current_query.match(/create\s+index\s+concurrently/i)
10
+ end
11
+ end
12
+
13
+ def self.autovacuum_regex
14
+ "^autovacuum: (?!.*to prevent wraparound)"
15
+ end
16
+
17
+ def self.find_blocking_transactions(minimum_transaction_age = "0 seconds")
18
+ pid_column, state_column = if ActiveRecord::Base.connection.select_value("SHOW server_version") =~ /9\.1/
19
+ ["procpid", "current_query"]
20
+ else
21
+ ["pid", "query"]
22
+ end
23
+
24
+ raw_query = <<-SQL
25
+ SELECT
26
+ psa.datname as database, -- Will only ever be one database
27
+ psa.#{state_column} as current_query,
28
+ clock_timestamp() - psa.xact_start AS transaction_age,
29
+ array_agg(distinct c.relname) AS tables_with_locks
30
+ FROM pg_stat_activity psa -- Cluster wide
31
+ JOIN pg_locks l ON (psa.#{pid_column} = l.pid) -- Cluster wide
32
+ JOIN pg_class c ON (l.relation = c.oid) -- Database wide
33
+ JOIN pg_namespace ns ON (c.relnamespace = ns.oid) -- Database wide
34
+ WHERE psa.#{pid_column} != pg_backend_pid()
35
+ AND ns.nspname != 'pg_catalog'
36
+ AND c.relkind = 'r'
37
+ AND psa.xact_start < clock_timestamp() - ?::interval
38
+ AND psa.#{state_column} !~ ?
39
+ AND (
40
+ -- Be explicit about this being for a single database -- it's already implicit in
41
+ -- the relations used, and if we don't restrict this we could get incorrect results
42
+ -- with oid collisions from pg_namespace and pg_class.
43
+ l.database = 0
44
+ OR l.database = (SELECT d.oid FROM pg_database d WHERE d.datname = current_database())
45
+ )
46
+ GROUP BY psa.datname, psa.#{state_column}, psa.xact_start
47
+ SQL
48
+
49
+ query = ActiveRecord::Base.send(:sanitize_sql_for_conditions, [raw_query, minimum_transaction_age, autovacuum_regex])
50
+
51
+ ActiveRecord::Base.structs_from_sql(LongRunningTransaction, query)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ require 'stringio'
2
+
3
+ module PgHaMigrations
4
+ class BlockingDatabaseTransactionsReporter
5
+ CHECK_DURATION = "30 seconds"
6
+
7
+ def self.run
8
+ blocking_transactions = get_blocking_transactions
9
+ has_transactions = blocking_transactions.values.flatten.present?
10
+ _puts(report(blocking_transactions)) if has_transactions
11
+ end
12
+
13
+ def self.report(transactions)
14
+ report = StringIO.new
15
+ report << "Potentially blocking transactions:\n"
16
+ transactions.each do |db_description, blocking_transactions|
17
+ report << "#{db_description}:\n"
18
+ if blocking_transactions.empty?
19
+ report << "\t(no long running transactions)\n\n"
20
+ else
21
+ blocking_transactions.each do |transaction|
22
+ report << "\t#{transaction.description}\n\n"
23
+ end
24
+ end
25
+
26
+ if blocking_transactions.any?(&:concurrent_index_creation?)
27
+ report << <<-eos.strip_heredoc.lines.map { |line| "\t#{line}" }.join
28
+ Warning: concurrent indexes are currently being built. If you have any other
29
+ migrations in this deploy that will attempt to create additional
30
+ concurrent indexes on the same physical database (even if the table
31
+ being indexes is on another dimension) those migrations will not be
32
+ able to complete until the in-progress index creations finish.
33
+
34
+ For more information, see #service-db in Slack.\n
35
+ eos
36
+ report << "\n" # Blank line intentional
37
+ end
38
+ end
39
+ report.string
40
+ end
41
+
42
+ def self.get_blocking_transactions
43
+ {
44
+ "Primary database" => PgHaMigrations::BlockingDatabaseTransactions.find_blocking_transactions(CHECK_DURATION)
45
+ }
46
+ end
47
+
48
+ def self._puts(msg)
49
+ puts msg
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ class PgHaMigrations::Railtie < Rails::Railtie
2
+ rake_tasks do
3
+ load 'tasks/blocking_transactions.rake'
4
+ end
5
+ end
@@ -0,0 +1,192 @@
1
+ module PgHaMigrations::SafeStatements
2
+ def safe_create_table(table, options={}, &block)
3
+ unsafe_create_table(table, options, &block)
4
+ end
5
+
6
+ def safe_create_enum_type(name, values=nil)
7
+ case values
8
+ when nil
9
+ raise ArgumentError, "safe_create_enum_type expects a set of values; if you want an enum with no values please pass an empty array"
10
+ when []
11
+ unsafe_execute("CREATE TYPE #{PG::Connection.quote_ident(name.to_s)} AS ENUM ()")
12
+ else
13
+ escaped_values = values.map do |value|
14
+ "'#{PG::Connection.escape_string(value.to_s)}'"
15
+ end
16
+ unsafe_execute("CREATE TYPE #{PG::Connection.quote_ident(name.to_s)} AS ENUM (#{escaped_values.join(',')})")
17
+ end
18
+ end
19
+
20
+ def safe_add_enum_value(name, value)
21
+ unsafe_execute("ALTER TYPE #{PG::Connection.quote_ident(name.to_s)} ADD VALUE '#{PG::Connection.escape_string(value)}'")
22
+ end
23
+
24
+ def safe_add_column(table, column, type, options = {})
25
+ if options.has_key? :default
26
+ raise PgHaMigrations::UnsafeMigrationError.new(":default is NOT SAFE! Use safe_change_column_default afterwards then backfill the data to prevent locking the table")
27
+ end
28
+ if options[:null] == false
29
+ raise PgHaMigrations::UnsafeMigrationError.new(":null => false is NOT SAFE if the table has data! If you _really_ want to do this, use unsafe_make_column_not_nullable")
30
+ end
31
+
32
+ unsafe_add_column(table, column, type, options)
33
+ end
34
+
35
+ def unsafe_add_column(table, column, type, options = {})
36
+ safely_acquire_lock_for_table(table) do
37
+ super(table, column, type, options)
38
+ end
39
+ end
40
+
41
+ def safe_change_column_default(table, column, default_value)
42
+ escaped_value = case default_value
43
+ when Proc
44
+ PG::Connection.escape_string(default_value.call.to_s)
45
+ else
46
+ "'#{PG::Connection.escape_string(default_value.to_s)}'"
47
+ end
48
+
49
+ safely_acquire_lock_for_table(table) do
50
+ unsafe_execute <<-SQL.strip_heredoc
51
+ ALTER TABLE #{PG::Connection.quote_ident(table.to_s)}
52
+ ALTER COLUMN #{PG::Connection.quote_ident(column.to_s)}
53
+ SET DEFAULT #{escaped_value}
54
+ SQL
55
+ end
56
+ end
57
+
58
+ def safe_make_column_nullable(table, column)
59
+ safely_acquire_lock_for_table(table) do
60
+ unsafe_execute "ALTER TABLE #{table} ALTER COLUMN #{column} DROP NOT NULL"
61
+ end
62
+ end
63
+
64
+ def unsafe_make_column_not_nullable(table, column, options={}) # options arg is only present for backwards compatiblity
65
+ safely_acquire_lock_for_table(table) do
66
+ unsafe_execute "ALTER TABLE #{table} ALTER COLUMN #{column} SET NOT NULL"
67
+ end
68
+ end
69
+
70
+ def safe_add_concurrent_index(table, columns, options={})
71
+ unsafe_add_index(table, columns, options.merge(:algorithm => :concurrently))
72
+ end
73
+
74
+ def safe_remove_concurrent_index(table, options={})
75
+ unless options.is_a?(Hash) && options.key?(:name)
76
+ raise ArgumentError, "Expected safe_remove_concurrent_index to be called with arguments (table_name, :name => ...)"
77
+ end
78
+ unless ActiveRecord::Base.connection.postgresql_version >= 90600
79
+ raise PgHaMigrations::InvalidMigrationError, "Removing an index concurrently is not supported on Postgres 9.1 databases"
80
+ end
81
+ index_size = select_value("SELECT pg_size_pretty(pg_relation_size('#{options[:name]}'))")
82
+ say "Preparing to drop index #{options[:name]} which is #{index_size} on disk..."
83
+ unsafe_remove_index(table, options.merge(:algorithm => :concurrently))
84
+ end
85
+
86
+ def safe_set_maintenance_work_mem_gb(gigabytes)
87
+ unsafe_execute("SET maintenance_work_mem = '#{PG::Connection.escape_string(gigabytes.to_s)} GB'")
88
+ end
89
+
90
+ def _per_migration_caller
91
+ @_per_migration_caller ||= Kernel.caller
92
+ end
93
+
94
+ def _check_postgres_adapter!
95
+ expected_adapter = "PostgreSQL"
96
+ actual_adapter = ActiveRecord::Base.connection.adapter_name
97
+ raise PgHaMigrations::UnsupportedAdapter, "This gem only works with the #{expected_adapter} adapter, found #{actual_adapter} instead" unless actual_adapter == expected_adapter
98
+ end
99
+
100
+ def exec_migration(conn, direction)
101
+ _check_postgres_adapter!
102
+ super(conn, direction)
103
+ end
104
+
105
+ def safely_acquire_lock_for_table(table, &block)
106
+ _check_postgres_adapter!
107
+ table = table.to_s
108
+ quoted_table_name = connection.quote_table_name(table)
109
+
110
+ successfully_acquired_lock = false
111
+
112
+ until successfully_acquired_lock
113
+ while (
114
+ blocking_transactions = PgHaMigrations::BlockingDatabaseTransactions.find_blocking_transactions("#{PgHaMigrations::LOCK_TIMEOUT_SECONDS} seconds")
115
+ blocking_transactions.any? { |query| query.tables_with_locks.include?(table) }
116
+ )
117
+ say "Waiting on blocking transactions:"
118
+ blocking_transactions.each do |blocking_transaction|
119
+ say blocking_transaction.description
120
+ end
121
+ sleep(PgHaMigrations::LOCK_TIMEOUT_SECONDS)
122
+ end
123
+
124
+ connection.transaction do
125
+ adjust_timeout_method = connection.postgresql_version >= 90300 ? :adjust_lock_timeout : :adjust_statement_timeout
126
+ begin
127
+ method(adjust_timeout_method).call(PgHaMigrations::LOCK_TIMEOUT_SECONDS) do
128
+ connection.execute("LOCK #{quoted_table_name};")
129
+ end
130
+ successfully_acquired_lock = true
131
+ rescue ActiveRecord::StatementInvalid => e
132
+ if e.message =~ /PG::LockNotAvailable.+ lock timeout/ || e.message =~ /PG::QueryCanceled.+ statement timeout/
133
+ sleep_seconds = PgHaMigrations::LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER * PgHaMigrations::LOCK_TIMEOUT_SECONDS
134
+ say "Timed out trying to acquire an exclusive lock on the #{quoted_table_name} table."
135
+ say "Sleeping for #{sleep_seconds}s to allow potentially queued up queries to finish before continuing."
136
+ sleep(sleep_seconds)
137
+
138
+ raise ActiveRecord::Rollback
139
+ else
140
+ raise e
141
+ end
142
+ end
143
+
144
+ if successfully_acquired_lock
145
+ block.call
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def adjust_lock_timeout(timeout_seconds = PgHaMigrations::LOCK_TIMEOUT_SECONDS, &block)
152
+ _check_postgres_adapter!
153
+ original_timeout = ActiveRecord::Base.value_from_sql("SHOW lock_timeout").sub(/s\Z/, '').to_i * 1000
154
+ begin
155
+ connection.execute("SET lock_timeout = #{PG::Connection.escape_string((timeout_seconds * 1000).to_s)};")
156
+ block.call
157
+ ensure
158
+ begin
159
+ connection.execute("SET lock_timeout = #{original_timeout};")
160
+ rescue ActiveRecord::StatementInvalid => e
161
+ if e.message =~ /PG::InFailedSqlTransaction/
162
+ # If we're in a failed transaction the `SET lock_timeout` will be rolled back,
163
+ # so we don't need to worry about cleaning up, and we can't execute SQL anyway.
164
+ else
165
+ raise e
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def adjust_statement_timeout(timeout_seconds, &block)
172
+ _check_postgres_adapter!
173
+ original_timeout = ActiveRecord::Base.value_from_sql("SHOW statement_timeout").sub(/s\Z/, '').to_i * 1000
174
+ begin
175
+ connection.execute("SET statement_timeout = #{PG::Connection.escape_string((timeout_seconds * 1000).to_s)};")
176
+ block.call
177
+ ensure
178
+ begin
179
+ connection.execute("SET statement_timeout = #{original_timeout};")
180
+ rescue ActiveRecord::StatementInvalid => e
181
+ if e.message =~ /PG::InFailedSqlTransaction/
182
+ # If we're in a failed transaction the `SET lock_timeout` will be rolled back,
183
+ # so we don't need to worry about cleaning up, and we can't execute SQL anyway.
184
+ else
185
+ raise e
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ ActiveRecord::Migration.send(:prepend, PgHaMigrations::SafeStatements)
@@ -0,0 +1,79 @@
1
+ module PgHaMigrations::UnsafeStatements
2
+ def self.delegate_unsafe_method_to_connection(method_name)
3
+ define_method("unsafe_#{method_name}") do |*args, &block|
4
+ arg_list = args.map { |arg| arg.inspect }.join(', ')
5
+ # say_with_time args taken from https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/migration.rb#L654
6
+ say_with_time "#{method_name}(#{arg_list})" do
7
+ connection.send(method_name, *args, &block)
8
+ end
9
+ end
10
+ end
11
+
12
+ delegate_unsafe_method_to_connection :create_table
13
+ def create_table(name, options={})
14
+ raise PgHaMigrations::UnsafeMigrationError.new(":create_table is NOT SAFE! Use safe_create_table instead")
15
+ end
16
+
17
+ delegate_unsafe_method_to_connection :add_column
18
+ def add_column(table, column, type, options={})
19
+ raise PgHaMigrations::UnsafeMigrationError.new(":add_column is NOT SAFE! Use safe_add_column instead")
20
+ end
21
+
22
+ delegate_unsafe_method_to_connection :change_table
23
+ def change_table(name, options={})
24
+ raise PgHaMigrations::UnsafeMigrationError.new(":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead")
25
+ end
26
+
27
+ delegate_unsafe_method_to_connection :drop_table
28
+ def drop_table(name)
29
+ raise PgHaMigrations::UnsafeMigrationError.new(":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed")
30
+ end
31
+
32
+ delegate_unsafe_method_to_connection :rename_table
33
+ def rename_table(old_name, new_name)
34
+ raise PgHaMigrations::UnsafeMigrationError.new(":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed")
35
+ end
36
+
37
+ delegate_unsafe_method_to_connection :rename_column
38
+ def rename_column(table_name, column_name, new_column_name)
39
+ raise PgHaMigrations::UnsafeMigrationError.new(":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed")
40
+ end
41
+
42
+ delegate_unsafe_method_to_connection :change_column
43
+ def change_column(table_name, column_name, type, options={})
44
+ raise PgHaMigrations::UnsafeMigrationError.new(":change_column is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead")
45
+ end
46
+
47
+ def change_column_null(table_name, column_name, null, default = nil)
48
+ raise PgHaMigrations::UnsafeMigrationError.new(<<-EOS.strip_heredoc)
49
+ :change_column_null is NOT (guaranteed to be) SAFE! Either use :safe_make_column_nullable or explicitly call :unsafe_make_column_not_nullable to proceed
50
+ EOS
51
+ end
52
+
53
+ delegate_unsafe_method_to_connection :remove_column
54
+ def remove_column(table_name, column_name, type, options={})
55
+ raise PgHaMigrations::UnsafeMigrationError.new(":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed")
56
+ end
57
+
58
+ delegate_unsafe_method_to_connection :add_index
59
+ def add_index(table_name, column_names, options={})
60
+ raise PgHaMigrations::UnsafeMigrationError.new(":add_index is NOT SAFE! Use safe_add_concurrent_index instead")
61
+ end
62
+
63
+ delegate_unsafe_method_to_connection :execute
64
+ def execute(sql, name = nil)
65
+ raise PgHaMigrations::UnsafeMigrationError.new(":execute is NOT SAFE! Explicitly call :unsafe_execute to proceed")
66
+ end
67
+
68
+ delegate_unsafe_method_to_connection :remove_index
69
+ def remove_index(table_name, options={})
70
+ raise PgHaMigrations::UnsafeMigrationError.new(":remove_index is NOT SAFE! Use safe_remove_concurrent_index instead for Postgres 9.6 databases; Explicitly call :unsafe_remove_index to proceed on Postgres 9.1")
71
+ end
72
+
73
+ delegate_unsafe_method_to_connection :add_foreign_key
74
+ def add_foreign_key(from_table, to_table, options)
75
+ raise PgHaMigrations::UnsafeMigrationError.new(":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key only if you have guidance from a migration reviewer in #service-app-db.")
76
+ end
77
+ end
78
+
79
+ ActiveRecord::Migration.send(:prepend, PgHaMigrations::UnsafeStatements)
@@ -0,0 +1,3 @@
1
+ module PgHaMigrations
2
+ VERSION = "0.1.3"
3
+ end
@@ -0,0 +1,9 @@
1
+ require_relative File.join("..", "pg_ha_migrations")
2
+
3
+ namespace :pg_ha_migrations do
4
+ desc "Check if blocking database transactions exist"
5
+ task :check_blocking_database_transactions do
6
+ PgHaMigrations::BlockingDatabaseTransactionsReporter.run
7
+ end
8
+ end
9
+
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "pg_ha_migrations/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pg_ha_migrations"
8
+ spec.version = PgHaMigrations::VERSION
9
+ spec.authors = ["jcoleman"]
10
+ spec.email = ["code@getbraintree.com"]
11
+
12
+ spec.summary = %q{}
13
+ spec.description = %q{}
14
+ spec.homepage = ""
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.15"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "pg"
28
+ spec.add_development_dependency "db-query-matchers", "~> 0.9.0"
29
+
30
+
31
+ spec.add_dependency "rails", ">= 5.0", "< 5.2"
32
+ spec.add_dependency "relation_to_struct"
33
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_ha_migrations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - jcoleman
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: db-query-matchers
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rails
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '5.0'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '5.2'
93
+ type: :runtime
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '5.0'
100
+ - - "<"
101
+ - !ruby/object:Gem::Version
102
+ version: '5.2'
103
+ - !ruby/object:Gem::Dependency
104
+ name: relation_to_struct
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description: ''
118
+ email:
119
+ - code@getbraintree.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - ".rspec"
126
+ - ".ruby-version"
127
+ - ".travis.yml"
128
+ - CODE_OF_CONDUCT.md
129
+ - Gemfile
130
+ - LICENSE.txt
131
+ - README.md
132
+ - Rakefile
133
+ - bin/console
134
+ - bin/setup
135
+ - lib/pg_ha_migrations.rb
136
+ - lib/pg_ha_migrations/allowed_versions.rb
137
+ - lib/pg_ha_migrations/blocking_database_transactions.rb
138
+ - lib/pg_ha_migrations/blocking_database_transactions_reporter.rb
139
+ - lib/pg_ha_migrations/railtie.rb
140
+ - lib/pg_ha_migrations/safe_statements.rb
141
+ - lib/pg_ha_migrations/unsafe_statements.rb
142
+ - lib/pg_ha_migrations/version.rb
143
+ - lib/tasks/blocking_transactions.rake
144
+ - pg_ha_migrations.gemspec
145
+ homepage: ''
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubyforge_project:
165
+ rubygems_version: 2.7.8
166
+ signing_key:
167
+ specification_version: 4
168
+ summary: ''
169
+ test_files: []