Merge pull request #485 from larskanis/wip
Revert connecting on a host-by-host basis and make sure only authentication errors stop host iteration
This commit is contained in:
commit
2ccb200fba
4
Rakefile
4
Rakefile
|
@ -16,14 +16,14 @@ LIBDIR = BASEDIR + 'lib'
|
|||
EXTDIR = BASEDIR + 'ext'
|
||||
PKGDIR = BASEDIR + 'pkg'
|
||||
TMPDIR = BASEDIR + 'tmp'
|
||||
TESTDIR = BASEDIR + "tmp_test_specs"
|
||||
TESTDIR = BASEDIR + "tmp_test_*"
|
||||
|
||||
DLEXT = RbConfig::CONFIG['DLEXT']
|
||||
EXT = LIBDIR + "pg_ext.#{DLEXT}"
|
||||
|
||||
GEMSPEC = 'pg.gemspec'
|
||||
|
||||
CLOBBER.include( TESTDIR.to_s )
|
||||
CLEAN.include( TESTDIR.to_s )
|
||||
CLEAN.include( PKGDIR.to_s, TMPDIR.to_s )
|
||||
CLEAN.include "lib/*/libpq.dll"
|
||||
CLEAN.include "lib/pg_ext.*"
|
||||
|
|
|
@ -555,14 +555,17 @@ class PG::Connection
|
|||
if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
|
||||
# Lowest timeout is 2 seconds - like in libpq
|
||||
timeo = [timeo, 2].max
|
||||
stop_time = timeo + Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
host_count = conninfo_hash[:host].to_s.count(",") + 1
|
||||
stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
|
||||
poll_status = PG::PGRES_POLLING_WRITING
|
||||
until poll_status == PG::PGRES_POLLING_OK ||
|
||||
poll_status == PG::PGRES_POLLING_FAILED
|
||||
|
||||
timeout = stop_time&.-(Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
||||
# Set single timeout to parameter "connect_timeout" but
|
||||
# don't exceed total connection time of number-of-hosts * connect_timeout.
|
||||
timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
|
||||
event = if !timeout || timeout >= 0
|
||||
# If the socket needs to read, wait 'til it becomes readable to poll again
|
||||
case poll_status
|
||||
|
@ -600,7 +603,6 @@ class PG::Connection
|
|||
|
||||
# Check to see if it's finished or failed yet
|
||||
poll_status = send( poll_meth )
|
||||
@last_status = status unless [PG::CONNECTION_BAD, PG::CONNECTION_OK].include?(status)
|
||||
end
|
||||
|
||||
unless status == PG::CONNECTION_OK
|
||||
|
@ -694,81 +696,47 @@ class PG::Connection
|
|||
errors = []
|
||||
if iopts[:hostaddr]
|
||||
# hostaddr is provided -> no need to resolve hostnames
|
||||
ihostaddrs = iopts[:hostaddr].split(",", -1)
|
||||
|
||||
ihosts = iopts[:host].split(",", -1) if iopts[:host]
|
||||
raise PG::ConnectionBad, "could not match #{ihosts.size} host names to #{ihostaddrs.size} hostaddr values" if ihosts && ihosts.size != ihostaddrs.size
|
||||
|
||||
iports = iopts[:port].split(",", -1)
|
||||
iports = iports * ihostaddrs.size if iports.size == 1
|
||||
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihostaddrs.size} hosts" if iports.size != ihostaddrs.size
|
||||
|
||||
# Try to connect to each hostaddr with separate timeout
|
||||
ihostaddrs.each_with_index do |ihostaddr, idx|
|
||||
oopts = iopts.merge(hostaddr: ihostaddr, port: iports[idx])
|
||||
oopts[:host] = ihosts[idx] if ihosts
|
||||
c = connect_internal(oopts, errors)
|
||||
return c if c
|
||||
end
|
||||
elsif iopts[:host] && !iopts[:host].empty?
|
||||
# Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
|
||||
elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
|
||||
# Resolve DNS in Ruby to avoid blocking state while connecting.
|
||||
# Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
|
||||
# This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
|
||||
ihosts = iopts[:host].split(",", -1)
|
||||
|
||||
iports = iopts[:port].split(",", -1)
|
||||
iports = iports * ihosts.size if iports.size == 1
|
||||
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
|
||||
|
||||
ihosts.each_with_index do |mhost, idx|
|
||||
dests = ihosts.each_with_index.flat_map do |mhost, idx|
|
||||
unless host_is_named_pipe?(mhost)
|
||||
addrs = if Fiber.respond_to?(:scheduler) &&
|
||||
if Fiber.respond_to?(:scheduler) &&
|
||||
Fiber.scheduler &&
|
||||
RUBY_VERSION < '3.1.'
|
||||
|
||||
# Use a second thread to avoid blocking of the scheduler.
|
||||
# `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
|
||||
Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
|
||||
hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
|
||||
else
|
||||
Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
|
||||
end
|
||||
|
||||
# Try to connect to each host with separate timeout
|
||||
addrs.each do |addr|
|
||||
oopts = iopts.merge(hostaddr: addr, host: mhost, port: iports[idx])
|
||||
c = connect_internal(oopts, errors)
|
||||
return c if c
|
||||
hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
|
||||
end
|
||||
else
|
||||
# No hostname to resolve (UnixSocket)
|
||||
oopts = iopts.merge(host: mhost, port: iports[idx])
|
||||
c = connect_internal(oopts, errors)
|
||||
return c if c
|
||||
hostaddrs = [nil]
|
||||
end
|
||||
hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
|
||||
end
|
||||
iopts.merge!(
|
||||
hostaddr: dests.map{|d| d[0] }.join(","),
|
||||
host: dests.map{|d| d[1] }.join(","),
|
||||
port: dests.map{|d| d[2] }.join(","))
|
||||
else
|
||||
# No host given
|
||||
return connect_internal(iopts)
|
||||
end
|
||||
raise PG::ConnectionBad, errors.join("\n")
|
||||
end
|
||||
conn = self.connect_start(iopts) or
|
||||
raise(PG::Error, "Unable to create a new connection")
|
||||
|
||||
private def connect_internal(opts, errors=nil)
|
||||
begin
|
||||
conn = self.connect_start(opts) or
|
||||
raise(PG::Error, "Unable to create a new connection")
|
||||
raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
|
||||
|
||||
raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
|
||||
|
||||
conn.send(:async_connect_or_reset, :connect_poll)
|
||||
rescue PG::ConnectionBad => err
|
||||
if errors && !(conn && [PG::CONNECTION_AWAITING_RESPONSE].include?(conn.instance_variable_get(:@last_status)))
|
||||
# Seems to be no authentication error -> try next host
|
||||
errors << err
|
||||
return nil
|
||||
else
|
||||
# Probably an authentication error
|
||||
raise
|
||||
end
|
||||
end
|
||||
conn.send(:async_connect_or_reset, :connect_poll)
|
||||
conn
|
||||
end
|
||||
|
||||
|
|
375
spec/helpers.rb
375
spec/helpers.rb
|
@ -9,7 +9,7 @@ require_relative 'helpers/scheduler.rb'
|
|||
require_relative 'helpers/tcp_gate_scheduler.rb'
|
||||
require_relative 'helpers/tcp_gate_switcher.rb'
|
||||
|
||||
DEFAULT_TEST_DIR_STR = File.join(Dir.pwd, "tmp_test_specs")
|
||||
DEFAULT_TEST_DIR_STR = Dir.pwd
|
||||
TEST_DIR_STR = ENV['RUBY_PG_TEST_DIR'] || DEFAULT_TEST_DIR_STR
|
||||
TEST_DIRECTORY = Pathname.new(TEST_DIR_STR)
|
||||
DATA_OBJ_MEMSIZE = 40
|
||||
|
@ -23,7 +23,25 @@ module PG::TestingHelpers
|
|||
if mod.respond_to?( :around )
|
||||
|
||||
mod.before( :all ) do
|
||||
@conn = connect_testing_db
|
||||
@port = $pg_server.port
|
||||
@conninfo = $pg_server.conninfo
|
||||
@unix_socket = $pg_server.unix_socket
|
||||
@conn = $pg_server.connect
|
||||
|
||||
# Find a local port that is not in use
|
||||
@port_down = @port + 10
|
||||
loop do
|
||||
@port_down = @port_down + 1
|
||||
begin
|
||||
TCPSocket.new("::1", @port_down)
|
||||
rescue SystemCallError
|
||||
begin
|
||||
TCPSocket.new("127.0.0.1", @port_down)
|
||||
rescue SystemCallError
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
mod.around( :each ) do |example|
|
||||
|
@ -93,96 +111,99 @@ module PG::TestingHelpers
|
|||
module_function
|
||||
###############
|
||||
|
||||
### Create a string that contains the ANSI codes specified and return it
|
||||
def ansi_code( *attributes )
|
||||
attributes.flatten!
|
||||
attributes.collect! {|at| at.to_s }
|
||||
module Loggable
|
||||
### Create a string that contains the ANSI codes specified and return it
|
||||
def ansi_code( *attributes )
|
||||
attributes.flatten!
|
||||
attributes.collect! {|at| at.to_s }
|
||||
|
||||
return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
|
||||
attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
|
||||
return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
|
||||
attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
|
||||
|
||||
# $stderr.puts " attr is: %p" % [attributes]
|
||||
if attributes.empty?
|
||||
return ''
|
||||
else
|
||||
return "\e[%sm" % attributes
|
||||
# $stderr.puts " attr is: %p" % [attributes]
|
||||
if attributes.empty?
|
||||
return ''
|
||||
else
|
||||
return "\e[%sm" % attributes
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
### Colorize the given +string+ with the specified +attributes+ and return it, handling
|
||||
### line-endings, color reset, etc.
|
||||
def colorize( *args )
|
||||
string = ''
|
||||
|
||||
if block_given?
|
||||
string = yield
|
||||
else
|
||||
string = args.shift
|
||||
end
|
||||
|
||||
ending = string[/(\s)$/] || ''
|
||||
string = string.rstrip
|
||||
|
||||
return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
|
||||
end
|
||||
|
||||
|
||||
### Output a message with highlighting.
|
||||
def message( *msg )
|
||||
$stderr.puts( colorize(:bold) { msg.flatten.join(' ') } )
|
||||
end
|
||||
|
||||
|
||||
### Output a logging message if $VERBOSE is true
|
||||
def trace( *msg )
|
||||
return unless $VERBOSE
|
||||
output = colorize( msg.flatten.join(' '), 'yellow' )
|
||||
$stderr.puts( output )
|
||||
end
|
||||
|
||||
|
||||
### Return the specified args as a string, quoting any that have a space.
|
||||
def quotelist( *args )
|
||||
return args.flatten.collect {|part| part.to_s =~ /\s/ ? part.to_s.inspect : part.to_s }
|
||||
end
|
||||
|
||||
|
||||
### Run the specified command +cmd+ with system(), failing if the execution
|
||||
### fails.
|
||||
def run( *cmd )
|
||||
cmd.flatten!
|
||||
|
||||
if cmd.length > 1
|
||||
trace( quotelist(*cmd) )
|
||||
else
|
||||
trace( cmd )
|
||||
end
|
||||
|
||||
system( *cmd )
|
||||
raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
|
||||
end
|
||||
|
||||
|
||||
### Run the specified command +cmd+ after redirecting stdout and stderr to the specified
|
||||
### +logpath+, failing if the execution fails.
|
||||
def log_and_run( logpath, *cmd )
|
||||
cmd.flatten!
|
||||
|
||||
if cmd.length > 1
|
||||
trace( quotelist(*cmd) )
|
||||
else
|
||||
trace( cmd )
|
||||
end
|
||||
|
||||
# Eliminate the noise of creating/tearing down the database by
|
||||
# redirecting STDERR/STDOUT to a logfile
|
||||
logfh = File.open( logpath, File::WRONLY|File::CREAT|File::APPEND )
|
||||
system( *cmd, [STDOUT, STDERR] => logfh )
|
||||
|
||||
raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
### Colorize the given +string+ with the specified +attributes+ and return it, handling
|
||||
### line-endings, color reset, etc.
|
||||
def colorize( *args )
|
||||
string = ''
|
||||
|
||||
if block_given?
|
||||
string = yield
|
||||
else
|
||||
string = args.shift
|
||||
end
|
||||
|
||||
ending = string[/(\s)$/] || ''
|
||||
string = string.rstrip
|
||||
|
||||
return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
|
||||
end
|
||||
|
||||
|
||||
### Output a message with highlighting.
|
||||
def message( *msg )
|
||||
$stderr.puts( colorize(:bold) { msg.flatten.join(' ') } )
|
||||
end
|
||||
|
||||
|
||||
### Output a logging message if $VERBOSE is true
|
||||
def trace( *msg )
|
||||
return unless $VERBOSE
|
||||
output = colorize( msg.flatten.join(' '), 'yellow' )
|
||||
$stderr.puts( output )
|
||||
end
|
||||
|
||||
|
||||
### Return the specified args as a string, quoting any that have a space.
|
||||
def quotelist( *args )
|
||||
return args.flatten.collect {|part| part.to_s =~ /\s/ ? part.to_s.inspect : part.to_s }
|
||||
end
|
||||
|
||||
|
||||
### Run the specified command +cmd+ with system(), failing if the execution
|
||||
### fails.
|
||||
def run( *cmd )
|
||||
cmd.flatten!
|
||||
|
||||
if cmd.length > 1
|
||||
trace( quotelist(*cmd) )
|
||||
else
|
||||
trace( cmd )
|
||||
end
|
||||
|
||||
system( *cmd )
|
||||
raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
|
||||
end
|
||||
|
||||
|
||||
### Run the specified command +cmd+ after redirecting stdout and stderr to the specified
|
||||
### +logpath+, failing if the execution fails.
|
||||
def log_and_run( logpath, *cmd )
|
||||
cmd.flatten!
|
||||
|
||||
if cmd.length > 1
|
||||
trace( quotelist(*cmd) )
|
||||
else
|
||||
trace( cmd )
|
||||
end
|
||||
|
||||
# Eliminate the noise of creating/tearing down the database by
|
||||
# redirecting STDERR/STDOUT to a logfile
|
||||
logfh = File.open( logpath, File::WRONLY|File::CREAT|File::APPEND )
|
||||
system( *cmd, [STDOUT, STDERR] => logfh )
|
||||
|
||||
raise "Command failed: [%s]" % [cmd.join(' ')] unless $?.success?
|
||||
end
|
||||
|
||||
extend Loggable
|
||||
|
||||
### Check the current directory for directories that look like they're
|
||||
### testing directories from previous tests, and tell any postgres instances
|
||||
|
@ -210,83 +231,115 @@ module PG::TestingHelpers
|
|||
end
|
||||
end
|
||||
|
||||
def define_testing_conninfo
|
||||
ENV['PGPORT'] ||= "54321"
|
||||
@port = ENV['PGPORT'].to_i
|
||||
ENV['PGHOST'] = 'localhost'
|
||||
td = TEST_DIRECTORY + 'data'
|
||||
@conninfo = "host=localhost port=#{@port} dbname=test sslrootcert=#{td + 'ruby-pg-ca-cert'} sslcert=#{td + 'ruby-pg-client-cert'} sslkey=#{td + 'ruby-pg-client-key'}"
|
||||
@unix_socket = TEST_DIRECTORY.to_s
|
||||
end
|
||||
class PostgresServer
|
||||
include Loggable
|
||||
|
||||
### Set up a PostgreSQL database instance for testing.
|
||||
def setup_testing_db( description )
|
||||
stop_existing_postmasters()
|
||||
attr_reader :port
|
||||
attr_reader :conninfo
|
||||
attr_reader :unix_socket
|
||||
|
||||
trace "Setting up test database for #{description}"
|
||||
@test_pgdata = TEST_DIRECTORY + 'data'
|
||||
@test_pgdata.mkpath
|
||||
### Set up a PostgreSQL database instance for testing.
|
||||
def initialize( name, port: 54321, postgresql_conf: '' )
|
||||
trace "Setting up test database for #{name}"
|
||||
@name = name
|
||||
@port = port
|
||||
@test_dir = TEST_DIRECTORY + "tmp_test_#{@name}"
|
||||
@test_pgdata = @test_dir + 'data'
|
||||
@test_pgdata.mkpath
|
||||
|
||||
define_testing_conninfo
|
||||
@logfile = @test_dir + 'setup.log'
|
||||
trace "Command output logged to #{@logfile}"
|
||||
|
||||
@logfile = TEST_DIRECTORY + 'setup.log'
|
||||
trace "Command output logged to #{@logfile}"
|
||||
begin
|
||||
unless (@test_pgdata+"postgresql.conf").exist?
|
||||
FileUtils.rm_rf( @test_pgdata, :verbose => $DEBUG )
|
||||
trace "Running initdb"
|
||||
log_and_run @logfile, 'initdb', '-E', 'UTF8', '--no-locale', '-D', @test_pgdata.to_s
|
||||
end
|
||||
|
||||
begin
|
||||
unless (@test_pgdata+"postgresql.conf").exist?
|
||||
FileUtils.rm_rf( @test_pgdata, :verbose => $DEBUG )
|
||||
trace "Running initdb"
|
||||
log_and_run @logfile, 'initdb', '-E', 'UTF8', '--no-locale', '-D', @test_pgdata.to_s
|
||||
end
|
||||
|
||||
unless (@test_pgdata+"ruby-pg-server-cert").exist?
|
||||
trace "Enable SSL"
|
||||
# Enable SSL in server config
|
||||
File.open(@test_pgdata+"postgresql.conf", "a+") do |fd|
|
||||
fd.puts <<-EOT
|
||||
unless (@test_pgdata+"ruby-pg-server-cert").exist?
|
||||
trace "Enable SSL"
|
||||
# Enable SSL in server config
|
||||
File.open(@test_pgdata+"postgresql.conf", "a+") do |fd|
|
||||
fd.puts <<-EOT
|
||||
ssl = on
|
||||
ssl_ca_file = 'ruby-pg-ca-cert'
|
||||
ssl_cert_file = 'ruby-pg-server-cert'
|
||||
ssl_key_file = 'ruby-pg-server-key'
|
||||
#{postgresql_conf}
|
||||
EOT
|
||||
end
|
||||
|
||||
# Enable MD5 authentication in hba config
|
||||
hba_content = File.read(@test_pgdata+"pg_hba.conf")
|
||||
File.open(@test_pgdata+"pg_hba.conf", "w") do |fd|
|
||||
fd.puts <<-EOT
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
host all testusermd5 ::1/128 md5
|
||||
EOT
|
||||
fd.puts hba_content
|
||||
end
|
||||
|
||||
trace "Generate certificates"
|
||||
generate_ssl_certs(@test_pgdata.to_s)
|
||||
end
|
||||
|
||||
trace "Generate certificates"
|
||||
generate_ssl_certs(@test_pgdata.to_s)
|
||||
trace "Starting postgres"
|
||||
sopt = "-p #{@port}"
|
||||
sopt += " -k #{@test_dir.to_s.dump}" unless RUBY_PLATFORM=~/mingw|mswin/i
|
||||
log_and_run @logfile, 'pg_ctl', '-w', '-o', sopt,
|
||||
'-D', @test_pgdata.to_s, 'start'
|
||||
sleep 2
|
||||
|
||||
td = @test_pgdata
|
||||
@conninfo = "host=localhost port=#{@port} dbname=test sslrootcert=#{td + 'ruby-pg-ca-cert'} sslcert=#{td + 'ruby-pg-client-cert'} sslkey=#{td + 'ruby-pg-client-key'}"
|
||||
@unix_socket = @test_dir.to_s
|
||||
rescue => err
|
||||
$stderr.puts "%p during test setup: %s" % [ err.class, err.message ]
|
||||
$stderr.puts "See #{@logfile} for details."
|
||||
$stderr.puts err.backtrace if $DEBUG
|
||||
fail
|
||||
end
|
||||
end
|
||||
|
||||
def generate_ssl_certs(output_dir)
|
||||
gen = CertGenerator.new(output_dir)
|
||||
|
||||
trace "create ca-key"
|
||||
ca_key = gen.create_key('ruby-pg-ca-key')
|
||||
ca_cert = gen.create_ca_cert('ruby-pg-ca-cert', ca_key, '/CN=ruby-pg root key')
|
||||
|
||||
trace "create server cert"
|
||||
key = gen.create_key('ruby-pg-server-key')
|
||||
csr = gen.create_signing_request('ruby-pg-server-csr', '/CN=localhost', key)
|
||||
gen.create_cert_from_csr('ruby-pg-server-cert', csr, ca_cert, ca_key, dns_names: %w[localhost] )
|
||||
|
||||
trace "create client cert"
|
||||
key = gen.create_key('ruby-pg-client-key')
|
||||
csr = gen.create_signing_request('ruby-pg-client-csr', '/CN=ruby-pg client', key)
|
||||
gen.create_cert_from_csr('ruby-pg-client-cert', csr, ca_cert, ca_key)
|
||||
end
|
||||
|
||||
def create_test_db
|
||||
trace "Creating the test DB"
|
||||
log_and_run @logfile, 'psql', '-p', @port.to_s, '-e', '-c', 'DROP DATABASE IF EXISTS test', 'postgres'
|
||||
log_and_run @logfile, 'createdb', '-p', @port.to_s, '-e', 'test'
|
||||
end
|
||||
|
||||
def connect
|
||||
conn = PG.connect( @conninfo )
|
||||
conn.set_notice_processor do |message|
|
||||
$stderr.puts( @name + ':' + message ) if $DEBUG
|
||||
end
|
||||
|
||||
trace "Starting postgres"
|
||||
unix_socket = ['-o', "-k #{TEST_DIRECTORY.to_s.dump}"] unless RUBY_PLATFORM=~/mingw|mswin/i
|
||||
log_and_run @logfile, 'pg_ctl', '-w', *unix_socket,
|
||||
'-D', @test_pgdata.to_s, 'start'
|
||||
sleep 2
|
||||
|
||||
trace "Creating the test DB"
|
||||
log_and_run @logfile, 'psql', '-e', '-c', 'DROP DATABASE IF EXISTS test', 'postgres'
|
||||
log_and_run @logfile, 'createdb', '-e', 'test'
|
||||
|
||||
rescue => err
|
||||
$stderr.puts "%p during test setup: %s" % [ err.class, err.message ]
|
||||
$stderr.puts "See #{@logfile} for details."
|
||||
$stderr.puts err.backtrace if $DEBUG
|
||||
fail
|
||||
end
|
||||
end
|
||||
|
||||
def connect_testing_db
|
||||
define_testing_conninfo
|
||||
conn = PG.connect( @conninfo )
|
||||
conn.set_notice_processor do |message|
|
||||
$stderr.puts( description + ':' + message ) if $DEBUG
|
||||
return conn
|
||||
end
|
||||
|
||||
return conn
|
||||
end
|
||||
def teardown
|
||||
trace "Tearing down test database for #{@name}"
|
||||
|
||||
def teardown_testing_db
|
||||
trace "Tearing down test database"
|
||||
|
||||
log_and_run @logfile, 'pg_ctl', '-D', @test_pgdata.to_s, 'stop'
|
||||
log_and_run @logfile, 'pg_ctl', '-D', @test_pgdata.to_s, 'stop'
|
||||
end
|
||||
end
|
||||
|
||||
class CertGenerator
|
||||
|
@ -398,24 +451,6 @@ EOT
|
|||
end
|
||||
end
|
||||
|
||||
def generate_ssl_certs(output_dir)
|
||||
gen = CertGenerator.new(output_dir)
|
||||
|
||||
trace "create ca-key"
|
||||
ca_key = gen.create_key('ruby-pg-ca-key')
|
||||
ca_cert = gen.create_ca_cert('ruby-pg-ca-cert', ca_key, '/CN=ruby-pg root key')
|
||||
|
||||
trace "create server cert"
|
||||
key = gen.create_key('ruby-pg-server-key')
|
||||
csr = gen.create_signing_request('ruby-pg-server-csr', '/CN=localhost', key)
|
||||
gen.create_cert_from_csr('ruby-pg-server-cert', csr, ca_cert, ca_key, dns_names: %w[localhost] )
|
||||
|
||||
trace "create client cert"
|
||||
key = gen.create_key('ruby-pg-client-key')
|
||||
csr = gen.create_signing_request('ruby-pg-client-csr', '/CN=ruby-pg client', key)
|
||||
gen.create_cert_from_csr('ruby-pg-client-cert', csr, ca_cert, ca_key)
|
||||
end
|
||||
|
||||
def check_for_lingering_connections( conn )
|
||||
conn.exec( "SELECT * FROM pg_stat_activity" ) do |res|
|
||||
conns = res.find_all {|row| row['pid'].to_i != conn.backend_pid && ["client backend", nil].include?(row["backend_type"]) }
|
||||
|
@ -653,9 +688,15 @@ RSpec.configure do |config|
|
|||
|
||||
### Automatically set up and tear down the database
|
||||
config.before(:suite) do |*args|
|
||||
PG::TestingHelpers.setup_testing_db("the spec suite")
|
||||
PG::TestingHelpers.stop_existing_postmasters
|
||||
|
||||
ENV['PGHOST'] = 'localhost'
|
||||
ENV['PGPORT'] ||= "54321"
|
||||
port = ENV['PGPORT'].to_i
|
||||
$pg_server = PG::TestingHelpers::PostgresServer.new("specs", port: port)
|
||||
$pg_server.create_test_db
|
||||
end
|
||||
config.after(:suite) do
|
||||
PG::TestingHelpers.teardown_testing_db
|
||||
$pg_server.teardown
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,11 +8,10 @@ require 'pg'
|
|||
|
||||
describe PG::Connection do
|
||||
|
||||
it "tries to connect to localhost with IPv6 and IPv4", :ipv6 do
|
||||
it "tries to connect to localhost with IPv6 and IPv4", :ipv6, :postgresql_10 do
|
||||
uri = "postgres://localhost:#{@port+1}/test"
|
||||
expect(described_class).to receive(:parse_connect_args).once.ordered.with(uri, any_args).and_call_original
|
||||
expect(described_class).to receive(:parse_connect_args).once.ordered.with(hash_including(hostaddr: "::1")).and_call_original
|
||||
expect(described_class).to receive(:parse_connect_args).once.ordered.with(hash_including(hostaddr: "127.0.0.1")).and_call_original
|
||||
expect(described_class).to receive(:parse_connect_args).once.ordered.with(hash_including(hostaddr: "::1,127.0.0.1")).and_call_original
|
||||
expect{ described_class.connect( uri ) }.to raise_error(PG::ConnectionBad)
|
||||
end
|
||||
|
||||
|
|
|
@ -289,8 +289,61 @@ describe PG::Connection do
|
|||
end
|
||||
end
|
||||
|
||||
context "with multiple PostgreSQL servers", :without_transaction do
|
||||
before :all do
|
||||
@port_ro = @port + 1
|
||||
@dbms = PG::TestingHelpers::PostgresServer.new("read-only",
|
||||
port: @port_ro,
|
||||
postgresql_conf: "default_transaction_read_only=on"
|
||||
)
|
||||
end
|
||||
|
||||
after :all do
|
||||
@dbms&.teardown
|
||||
end
|
||||
|
||||
it "honors target_session_attrs requirements", :postgresql_10 do
|
||||
uri = "postgres://localhost:#{@port_ro},localhost:#{@port}/postgres?target_session_attrs=read-write"
|
||||
PG.connect(uri) do |conn|
|
||||
expect( conn.port ).to eq( @port )
|
||||
end
|
||||
|
||||
uri = "postgres://localhost:#{@port_ro},localhost:#{@port}/postgres?target_session_attrs=any"
|
||||
PG.connect(uri) do |conn|
|
||||
expect( conn.port ).to eq( @port_ro )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "stops hosts iteration on authentication errors", :without_transaction, :ipv6, :postgresql_10 do
|
||||
@conn.exec("DROP USER IF EXISTS testusermd5")
|
||||
@conn.exec("CREATE USER testusermd5 PASSWORD 'secret'")
|
||||
|
||||
uri = "host=::1,::1,127.0.0.1 port=#{@port_down},#{@port},#{@port} dbname=postgres user=testusermd5 password=wrong"
|
||||
error_match = if RUBY_PLATFORM=~/mingw|mswin/
|
||||
# It's a long standing issue of libpq, that the error text is not correctly returned when both client and server are running on Windows.
|
||||
# Instead a "Connection refused" is retured.
|
||||
/authenti.*testusermd5|Connection refused|server closed the connection unexpectedly/i
|
||||
else
|
||||
/authenti.*testusermd5/i
|
||||
end
|
||||
expect { PG.connect(uri) }.to raise_error(error_match)
|
||||
|
||||
uri = "host=::1,::1,127.0.0.1 port=#{@port_down},#{@port},#{@port} dbname=postgres user=testusermd5 password=secret"
|
||||
PG.connect(uri) do |conn|
|
||||
expect( conn.host ).to eq( "::1" )
|
||||
expect( conn.port ).to eq( @port )
|
||||
end
|
||||
|
||||
uri = "host=::1,::1,127.0.0.1 port=#{@port_down},#{@port_down},#{@port} dbname=postgres user=testusermd5 password=wrong"
|
||||
PG.connect(uri) do |conn|
|
||||
expect( conn.host ).to eq( "127.0.0.1" )
|
||||
expect( conn.port ).to eq( @port )
|
||||
end
|
||||
end
|
||||
|
||||
it "connects using URI with multiple hosts", :postgresql_12 do
|
||||
uri = "postgres://localhost:#{@port+1},127.0.0.1:#{@port}/test?keepalives=1"
|
||||
uri = "postgres://localhost:#{@port_down},127.0.0.1:#{@port}/test?keepalives=1"
|
||||
tmpconn = described_class.connect( uri )
|
||||
expect( tmpconn.status ).to eq( PG::CONNECTION_OK )
|
||||
expect( tmpconn.port ).to eq( @port )
|
||||
|
|
|
@ -7,7 +7,7 @@ context "running with sync_* methods" do
|
|||
before :all do
|
||||
@conn.finish
|
||||
PG::Connection.async_api = false
|
||||
@conn = connect_testing_db
|
||||
@conn = $pg_server.connect
|
||||
end
|
||||
|
||||
after :all do
|
||||
|
|
|
@ -77,7 +77,7 @@ context "with a Fiber scheduler", :scheduler do
|
|||
conninfo = @conninfo_gate.gsub(/(^| )host=\w+/, " host=scheduler-localhost")
|
||||
conn = PG.connect(conninfo)
|
||||
opt = conn.conninfo.find { |info| info[:keyword] == 'host' }
|
||||
expect( opt[:val] ).to eq( 'scheduler-localhost' )
|
||||
expect( opt[:val] ).to start_with( 'scheduler-localhost' )
|
||||
conn.finish
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue