pg_failover 1.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 940fc1e1a1faf10f7dbc18910a51f53a724b276dc5ad4e377251bc205dcf3291
4
+ data.tar.gz: 7b888845de98b57b6724cc0144c946ec27df321d02e025310dd7c04ed2a87cdf
5
+ SHA512:
6
+ metadata.gz: 32d0f7a5aee56766df278c53bba22ab439ec5914f96cd4c4aaeca97c5279aa9447fdfb1457f77015d15ca280f74ab01a2e17d8b7972660e6fc50618e7f512478
7
+ data.tar.gz: e0dc8a6d86ab6c6902dd37435a601a7095a4da366ce9da69001ead7ad3e257ae693d7ea66b82a051b6eb459bffe4cac525e43a705ca57eb8bcace829b9f5c962
@@ -0,0 +1,104 @@
1
+ version: 2.1
2
+ commands:
3
+ install-dependencies:
4
+ steps:
5
+ - restore_cache:
6
+ keys:
7
+ - pg_failover-{{ checksum "pg_failover.gemspec" }}-{{ checksum "Gemfile" }}
8
+ - run:
9
+ name: Install Ruby gems
10
+ command: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle
11
+ - save_cache:
12
+ key: pg_failover-{{ checksum "pg_failover.gemspec" }}-{{ checksum "Gemfile" }}
13
+ paths:
14
+ - vendor/bundle
15
+ test:
16
+ steps:
17
+ - run:
18
+ name: Run unit tests
19
+ command: bundle exec rspec
20
+
21
+ executors:
22
+ ruby:
23
+ docker:
24
+ - image: circleci/ruby:<< parameters.tag >>
25
+ parameters:
26
+ tag:
27
+ description: "Docker image tag"
28
+ default: "latest"
29
+ type: string
30
+
31
+ jobs:
32
+ build:
33
+ executor:
34
+ name: ruby
35
+ tag: << parameters.tag >>
36
+ parameters:
37
+ tag:
38
+ description: "Docker image tag"
39
+ default: << parameters.tag >>
40
+ type: string
41
+ steps:
42
+ - checkout
43
+ - install-dependencies
44
+ - test
45
+ publish:
46
+ executor:
47
+ name: ruby
48
+ steps:
49
+ - checkout
50
+ - install-dependencies
51
+ - run:
52
+ name: Build gem
53
+ command: bundle exec rake build
54
+ - run:
55
+ name: Configure RubyGems API key
56
+ command: |
57
+ mkdir -p ~/.gem
58
+ echo ":rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
59
+ chmod 0600 ~/.gem/credentials
60
+ - run:
61
+ name: Publish gem
62
+ command: |
63
+ package=$(ls -t1 pkg/pg_failover-*.gem | head -1)
64
+ gem push "$package"
65
+
66
+ workflows:
67
+ test-and-publish:
68
+ jobs:
69
+ - build:
70
+ filters:
71
+ tags:
72
+ only: /.*/
73
+ name: "ruby23"
74
+ tag: "2.3"
75
+ - build:
76
+ filters:
77
+ tags:
78
+ only: /.*/
79
+ name: "ruby24"
80
+ tag: "2.4"
81
+ - build:
82
+ filters:
83
+ tags:
84
+ only: /.*/
85
+ name: "ruby25"
86
+ tag: "2.5"
87
+ - build:
88
+ filters:
89
+ tags:
90
+ only: /.*/
91
+ name: "ruby26"
92
+ tag: "2.6"
93
+ - publish:
94
+ context: org-rubygems
95
+ filters:
96
+ branches:
97
+ ignore: /.*/
98
+ tags:
99
+ only: /^v\d{1,2}\.\d{1,2}\.\d{1,2}.*/
100
+ requires:
101
+ - ruby23
102
+ - ruby24
103
+ - ruby25
104
+ - ruby26
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.rspec_status
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ v1.0.0 Initial release.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ group :development, :test do
4
+ gem 'activerecord'
5
+ gem 'pg'
6
+ gem 'sequel'
7
+ end
8
+
9
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright © 2019, Funding Circle.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its contributors
15
+ may be used to endorse or promote products derived from this software without
16
+ specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # PgFailover
2
+
3
+ Handle potential failover events in PostgreSQL database connections by
4
+ reconnecting if the database is in a recovery mode. This check occurs when a
5
+ connection is checked out of the connection pool.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'pg_failover'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install pg_failover
22
+
23
+ ## Usage
24
+
25
+ This library can be configured via a code block or environment variables.
26
+
27
+ The configuration aspects are as follows:
28
+ - Enabled - Hooks a callback to the `pg` adapter.
29
+ - Max retries - How many times an attempt to reconnect should be made.
30
+ - Throttle interval - The period between checks on database connections.
31
+
32
+ In an initializer:
33
+
34
+ ```ruby
35
+ PgFailover.configure do |config|
36
+ config.enabled = true
37
+ config.max_retries = 3
38
+ config.throttle_interval = 3.3
39
+ end
40
+ ```
41
+
42
+ You can also configure the logging device to be used:
43
+
44
+ ```ruby
45
+ PgFailover.configure do |config|
46
+ config.logger = Rails.logger
47
+ end
48
+ ```
49
+
50
+ Using environment variables:
51
+
52
+ POSTGRES_FAILOVER_ENABLED=true
53
+ POSTGRES_FAILOVER_MAX_RETRIES=3
54
+ POSTGRES_FAILOVER_THROTTLE_INTERVAL=3.3
55
+
56
+ The above settings demonstrate enabling the failover checks and will only attempt to
57
+ re-establish a database connection 3 times with a pause of 3.3 seconds between
58
+ each attempt.
59
+
60
+ ## Development
61
+
62
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
63
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
64
+ prompt that will allow you to experiment.
65
+
66
+ To install this gem onto your local machine, run `bundle exec rake install`. To
67
+ release a new version, update the version number in `pg_failover.gemspec`, and
68
+ then run `bundle exec rake release`, which will create a git tag for the
69
+ version, push git commits and tags, and push the `.gem` file to
70
+ [rubygems.org](https://rubygems.org).
71
+
72
+ ## Contributing
73
+
74
+ Bug reports and pull requests are welcome on GitHub at https://github.com/FundingCircle/pg_failover.
75
+
76
+ ## License
77
+
78
+ Copyright © 2019 Funding Circle.
79
+
80
+ Distributed under the BSD 3-Clause License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pg_failover"
5
+ require "irb"
6
+
7
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgFailover
4
+ class ActiveRecordAdapter
5
+ def self.enable
6
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.set_callback(:checkout, :after) do
7
+ # this seems to be the connection pool that internaly has the connection loaded for the current thread
8
+ # methods like #execute and #reconnect! are against that connection
9
+ connection = self
10
+
11
+ PgFailover.connection_validator.call(
12
+ throttle_by: connection.raw_connection,
13
+ in_recovery: proc {
14
+ result = connection.execute('select pg_is_in_recovery()').first
15
+
16
+ %w[1 t true].include?(result['pg_is_in_recovery'].to_s)
17
+ },
18
+ reconnect: proc { connection.reconnect! }
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgFailover
4
+ Config = Struct.new(:enabled, :logger, :max_retries, :throttle_interval) do
5
+ def logger
6
+ self[:logger] ||= Logger.new($stdout)
7
+ end
8
+
9
+ def throttle_interval
10
+ self[:throttle_interval] ||= (ENV['POSTGRES_FAILOVER_THROTTLE_INTERVAL'] || 10.0).to_f
11
+ end
12
+
13
+ def throttle_enabled?
14
+ !throttle_interval.zero?
15
+ end
16
+
17
+ def max_retries
18
+ self[:max_retries] ||= (ENV['POSTGRES_FAILOVER_MAX_RETRIES'] || 1).to_i
19
+ end
20
+
21
+ def enabled
22
+ self[:enabled] ||= %w(1 t true).include?(ENV['POSTGRES_FAILOVER_ENABLED'])
23
+ end
24
+
25
+ def enabled?
26
+ !!enabled
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgFailover
4
+ class ConnectionValidator
5
+ def initialize(config)
6
+ @logger = config.logger
7
+ @max_retries = config.max_retries
8
+ @throttle = Throttle.new(throttle_interval: config.throttle_interval)
9
+ @config = config
10
+ end
11
+
12
+ attr_reader :logger, :max_retries, :throttle, :throttle_interval, :config
13
+
14
+ def call(in_recovery:, reconnect:, throttle_by:)
15
+ if config.throttle_enabled?
16
+ throttle.on_stale(throttle_by) { check_and_reconnect(in_recovery, reconnect) }
17
+ else
18
+ check_and_reconnect(in_recovery, reconnect)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def check_if_db_is_in_recovery(in_recovery)
25
+ in_recovery.call
26
+ rescue StandardError => e
27
+ logger.error("Got an error while trying to check pg_is_in_recovery, #{e.class}\n#{e.message}")
28
+ true
29
+ end
30
+
31
+ def check_and_reconnect(in_recovery, reconnect)
32
+ reconnect_attempts = 0
33
+
34
+ while (connection_in_recovery = check_if_db_is_in_recovery(in_recovery))
35
+ logger.info("The database is in recovery. Trying to reconnect. Attempt #{reconnect_attempts} from #{@max_retries}")
36
+ reconnect.call
37
+
38
+ reconnect_attempts += 1
39
+
40
+ break if reconnect_attempts >= @max_retries
41
+
42
+ sleep(rand(0..0.2))
43
+ end
44
+
45
+ !connection_in_recovery
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgFailover
4
+ module SequelAdapter
5
+ class << self
6
+ def register_extension
7
+ ::Sequel::Database.register_extension(:postgres_failover_validator) do |db|
8
+ db.pool.extend(PgFailover::SequelAdapter::ConnectionValidator)
9
+ end
10
+ end
11
+
12
+ def enable(databases = Sequel::DATABASES)
13
+ register_extension
14
+
15
+ databases.each do |db|
16
+ db.extension :postgres_failover_validator if db.adapter_scheme == :postgres
17
+ end
18
+ end
19
+ end
20
+
21
+ module ConnectionValidator
22
+ def acquire(*a)
23
+ connection = super
24
+
25
+ PgFailover.connection_validator.call(
26
+ throttle_by: connection,
27
+ in_recovery: proc {
28
+ result = connection.execute('select pg_is_in_recovery()') { |r| r.to_a }.first
29
+ %w[1 t true].include?(result['pg_is_in_recovery'].to_s)
30
+ },
31
+ reconnect: proc {
32
+ # This disconnect is copy pasted from the
33
+ # https://github.com/jeremyevans/sequel/blob/5.15.0/lib/sequel/extensions/connection_validator.rb#L103-L109
34
+ #
35
+ if pool_type == :sharded_threaded
36
+ sync{allocated(a.last).delete(Thread.current)}
37
+ else
38
+ sync{@allocated.delete(Thread.current)}
39
+ end
40
+
41
+ disconnect_connection(connection)
42
+
43
+ connection = super
44
+ }
45
+ )
46
+
47
+ connection
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgFailover
4
+ class Throttle
5
+ def initialize(throttle_interval: nil)
6
+ @last_good_at = {}
7
+ @throttle_interval = throttle_interval || 0.0
8
+ end
9
+
10
+ attr_reader :throttle_interval
11
+
12
+ def size
13
+ @last_good_at.count
14
+ end
15
+
16
+ def on_stale(connection)
17
+ return if should_throttle?(connection)
18
+
19
+ clear_stale_throttle_times
20
+
21
+ valid = yield
22
+
23
+ @last_good_at[connection] = Time.now.to_f if valid
24
+ end
25
+
26
+ def should_throttle?(connection)
27
+ return if @last_good_at[connection].nil?
28
+
29
+ connection_check_age = (Time.now.to_f - @last_good_at[connection])
30
+ connection_check_age < throttle_interval
31
+ end
32
+
33
+ def known?(connection)
34
+ @last_good_at[connection]
35
+ end
36
+
37
+ private
38
+
39
+ def clear_stale_throttle_times
40
+ stale_after = Time.now.to_f - throttle_interval * 3
41
+
42
+ @last_good_at.delete_if { |_k, v| stale_after > v }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ module PgFailover
2
+ autoload :ActiveRecordAdapter, 'pg_failover/active_record_adapter'
3
+ autoload :Config, 'pg_failover/config'
4
+ autoload :ConnectionValidator, 'pg_failover/connection_validator'
5
+ autoload :SequelAdapter, 'pg_failover/sequel_adapter'
6
+ autoload :Throttle, 'pg_failover/throttle'
7
+
8
+ class << self
9
+ def configure
10
+ yield configuration if block_given?
11
+
12
+ if configuration.enabled?
13
+ if configuration.throttle_enabled?
14
+ configuration.logger.info("Enabled PgFailover policy (one check per #{configuration.throttle_interval} seconds per connection on checkout)")
15
+ else
16
+ configuration.logger.info('Enabled PgFailover policy (one check for every checkout from the connection pool)')
17
+ end
18
+
19
+ SequelAdapter.enable if defined?(::Sequel)
20
+ ActiveRecordAdapter.enable if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
21
+
22
+ else
23
+ configuration.logger.warn 'Disabled PgFailover policy'
24
+ end
25
+ end
26
+
27
+ def connection_validator
28
+ @connection_validator ||= ConnectionValidator.new(configuration)
29
+ end
30
+
31
+ def configuration
32
+ @configuration ||= Config.new
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "pg_failover"
3
+ spec.version = "1.0.0-alpha"
4
+ spec.authors = ["Aleksandar Ivanov", "Andy Chambers", "Sasha Gerrand"]
5
+ spec.email = ["engineering+pg_failover@fundingcircle.com"]
6
+ spec.license = "BSD-3-Clause"
7
+
8
+ spec.summary = %q{Handle Postgres failover events gracefully.}
9
+ spec.description = %q{Handle Postgres failover events gracefully using your favourite ORM.}
10
+ spec.homepage = "https://github.com/FundingCircle/pg_failover"
11
+
12
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
13
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
14
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes." unless spec.respond_to?(:metadata)
15
+
16
+ spec.metadata = {
17
+ "changelog_uri" => "https://github.com/FundingCircle/pg_failover/blob/master/CHANGELOG.md",
18
+ "homepage_uri" => spec.homepage,
19
+ "source_code_uri" => "https://github.com/FundingCircle/pg_failover",
20
+ }
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_development_dependency "bundler", "~> 1.17"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_failover
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre.alpha
5
+ platform: ruby
6
+ authors:
7
+ - Aleksandar Ivanov
8
+ - Andy Chambers
9
+ - Sasha Gerrand
10
+ autorequire:
11
+ bindir: exe
12
+ cert_chain: []
13
+ date: 2019-02-06 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '1.17'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.17'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rake
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '10.0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '10.0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: rspec
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '3.0'
57
+ description: Handle Postgres failover events gracefully using your favourite ORM.
58
+ email:
59
+ - engineering+pg_failover@fundingcircle.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".circleci/config.yml"
65
+ - ".gitignore"
66
+ - ".rspec"
67
+ - CHANGELOG.md
68
+ - Gemfile
69
+ - LICENSE
70
+ - README.md
71
+ - Rakefile
72
+ - bin/console
73
+ - bin/setup
74
+ - lib/pg_failover.rb
75
+ - lib/pg_failover/active_record_adapter.rb
76
+ - lib/pg_failover/config.rb
77
+ - lib/pg_failover/connection_validator.rb
78
+ - lib/pg_failover/sequel_adapter.rb
79
+ - lib/pg_failover/throttle.rb
80
+ - pg_failover.gemspec
81
+ homepage: https://github.com/FundingCircle/pg_failover
82
+ licenses:
83
+ - BSD-3-Clause
84
+ metadata:
85
+ changelog_uri: https://github.com/FundingCircle/pg_failover/blob/master/CHANGELOG.md
86
+ homepage_uri: https://github.com/FundingCircle/pg_failover
87
+ source_code_uri: https://github.com/FundingCircle/pg_failover
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">"
100
+ - !ruby/object:Gem::Version
101
+ version: 1.3.1
102
+ requirements: []
103
+ rubygems_version: 3.0.1
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Handle Postgres failover events gracefully.
107
+ test_files: []