Skip to content

Commit 40b2ad5

Browse files
committed
Revert connecting on a host-by-host basis
.. in favor of passing all hosts to libpq at once and instead adjust connect_timeout handling roughtly to how libpq handles it. The problem is that libpg aborts connecting to multiple hosts, if there's a authentication failure. But if pg imitates this behaviour, the libpq API doesn't give an exact indication, whether the connection aborted due to an authentication error or due to some other error, which continues the host iteration. So we can not distinguish between an authentication error and other types of errors, other then by the error message. But there's the next problem, that the error message is locale dependent and that when both client and server are running on Windows, the error message is often not correctly delivered, which is a known long standing PostgreSQL issue. This commit therefore changes the execution back to how multiple hosts were handled similar to pg-1.3.x, but with two fixes: 1. Multiple IP addresses to one hostname are handled correctly, (fixes #452) 2. and connect_timeout is handled roughly like libpq. (fixes #450) It's only roughly, since the timeout is not strictly per host, but per single socket event, but with a total timeout multiplied with the number-of-hosts. Exact handling of connect_timeout like libpq is only possible if we connect host-by-host.
1 parent 801a70f commit 40b2ad5

File tree

3 files changed

+26
-58
lines changed

3 files changed

+26
-58
lines changed

lib/pg/connection.rb

Lines changed: 23 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -555,14 +555,17 @@ def cancel
555555
if (timeo = conninfo_hash[:connect_timeout].to_i) && timeo > 0
556556
# Lowest timeout is 2 seconds - like in libpq
557557
timeo = [timeo, 2].max
558-
stop_time = timeo + Process.clock_gettime(Process::CLOCK_MONOTONIC)
558+
host_count = conninfo_hash[:host].to_s.count(",") + 1
559+
stop_time = timeo * host_count + Process.clock_gettime(Process::CLOCK_MONOTONIC)
559560
end
560561

561562
poll_status = PG::PGRES_POLLING_WRITING
562563
until poll_status == PG::PGRES_POLLING_OK ||
563564
poll_status == PG::PGRES_POLLING_FAILED
564565

565-
timeout = stop_time&.-(Process.clock_gettime(Process::CLOCK_MONOTONIC))
566+
# Set single timeout to parameter "connect_timeout" but
567+
# don't exceed total connection time of number-of-hosts * connect_timeout.
568+
timeout = [timeo, stop_time - Process.clock_gettime(Process::CLOCK_MONOTONIC)].min if stop_time
566569
event = if !timeout || timeout >= 0
567570
# If the socket needs to read, wait 'til it becomes readable to poll again
568571
case poll_status
@@ -693,81 +696,47 @@ def new(*args)
693696
errors = []
694697
if iopts[:hostaddr]
695698
# hostaddr is provided -> no need to resolve hostnames
696-
ihostaddrs = iopts[:hostaddr].split(",", -1)
697699

698-
ihosts = iopts[:host].split(",", -1) if iopts[:host]
699-
raise PG::ConnectionBad, "could not match #{ihosts.size} host names to #{ihostaddrs.size} hostaddr values" if ihosts && ihosts.size != ihostaddrs.size
700-
701-
iports = iopts[:port].split(",", -1)
702-
iports = iports * ihostaddrs.size if iports.size == 1
703-
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihostaddrs.size} hosts" if iports.size != ihostaddrs.size
704-
705-
# Try to connect to each hostaddr with separate timeout
706-
ihostaddrs.each_with_index do |ihostaddr, idx|
707-
oopts = iopts.merge(hostaddr: ihostaddr, port: iports[idx])
708-
oopts[:host] = ihosts[idx] if ihosts
709-
c = connect_internal(oopts, errors)
710-
return c if c
711-
end
712-
elsif iopts[:host] && !iopts[:host].empty?
713-
# Resolve DNS in Ruby to avoid blocking state while connecting, when it ...
700+
elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000
701+
# Resolve DNS in Ruby to avoid blocking state while connecting.
702+
# Multiple comma-separated values are generated, if the hostname resolves to both IPv4 and IPv6 addresses.
703+
# This requires PostgreSQL-10+, so no DNS resolving is done on earlier versions.
714704
ihosts = iopts[:host].split(",", -1)
715-
716705
iports = iopts[:port].split(",", -1)
717706
iports = iports * ihosts.size if iports.size == 1
718707
raise PG::ConnectionBad, "could not match #{iports.size} port numbers to #{ihosts.size} hosts" if iports.size != ihosts.size
719708

720-
ihosts.each_with_index do |mhost, idx|
709+
dests = ihosts.each_with_index.flat_map do |mhost, idx|
721710
unless host_is_named_pipe?(mhost)
722-
addrs = if Fiber.respond_to?(:scheduler) &&
711+
if Fiber.respond_to?(:scheduler) &&
723712
Fiber.scheduler &&
724713
RUBY_VERSION < '3.1.'
725714

726715
# Use a second thread to avoid blocking of the scheduler.
727716
# `TCPSocket.gethostbyname` isn't fiber aware before ruby-3.1.
728-
Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
717+
hostaddrs = Thread.new{ Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue [''] }.value
729718
else
730-
Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
731-
end
732-
733-
# Try to connect to each host with separate timeout
734-
addrs.each do |addr|
735-
oopts = iopts.merge(hostaddr: addr, host: mhost, port: iports[idx])
736-
c = connect_internal(oopts, errors)
737-
return c if c
719+
hostaddrs = Addrinfo.getaddrinfo(mhost, nil, nil, :STREAM).map(&:ip_address) rescue ['']
738720
end
739721
else
740722
# No hostname to resolve (UnixSocket)
741-
oopts = iopts.merge(host: mhost, port: iports[idx])
742-
c = connect_internal(oopts, errors)
743-
return c if c
723+
hostaddrs = [nil]
744724
end
725+
hostaddrs.map { |hostaddr| [hostaddr, mhost, iports[idx]] }
745726
end
727+
iopts.merge!(
728+
hostaddr: dests.map{|d| d[0] }.join(","),
729+
host: dests.map{|d| d[1] }.join(","),
730+
port: dests.map{|d| d[2] }.join(","))
746731
else
747732
# No host given
748-
return connect_internal(iopts)
749733
end
750-
raise PG::ConnectionBad, errors.join("\n")
751-
end
752-
753-
private def connect_internal(opts, errors=nil)
754-
begin
755-
conn = self.connect_start(opts) or
756-
raise(PG::Error, "Unable to create a new connection")
734+
conn = self.connect_start(iopts) or
735+
raise(PG::Error, "Unable to create a new connection")
757736

758-
raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
737+
raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD
759738

760-
conn.send(:async_connect_or_reset, :connect_poll)
761-
rescue PG::ConnectionBad => err
762-
if errors && /authenticat/ !~ err.message
763-
# Seems to be no authentication error -> try next host
764-
errors << err
765-
return nil
766-
else
767-
# Probably an authentication error
768-
raise err
769-
end
770-
end
739+
conn.send(:async_connect_or_reset, :connect_poll)
771740
conn
772741
end
773742

spec/pg/connection_async_spec.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88

99
describe PG::Connection do
1010

11-
it "tries to connect to localhost with IPv6 and IPv4", :ipv6 do
11+
it "tries to connect to localhost with IPv6 and IPv4", :ipv6, :postgresql_10 do
1212
uri = "postgres://localhost:#{@port+1}/test"
1313
expect(described_class).to receive(:parse_connect_args).once.ordered.with(uri, any_args).and_call_original
14-
expect(described_class).to receive(:parse_connect_args).once.ordered.with(hash_including(hostaddr: "::1")).and_call_original
15-
expect(described_class).to receive(:parse_connect_args).once.ordered.with(hash_including(hostaddr: "127.0.0.1")).and_call_original
14+
expect(described_class).to receive(:parse_connect_args).once.ordered.with(hash_including(hostaddr: "::1,127.0.0.1")).and_call_original
1615
expect{ described_class.connect( uri ) }.to raise_error(PG::ConnectionBad)
1716
end
1817

spec/pg/scheduler_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
conninfo = @conninfo_gate.gsub(/(^| )host=\w+/, " host=scheduler-localhost")
7878
conn = PG.connect(conninfo)
7979
opt = conn.conninfo.find { |info| info[:keyword] == 'host' }
80-
expect( opt[:val] ).to eq( 'scheduler-localhost' )
80+
expect( opt[:val] ).to start_with( 'scheduler-localhost' )
8181
conn.finish
8282
end
8383
end

0 commit comments

Comments
 (0)