pg_failover 1.0.0.pre.alpha

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: 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: []