diff --git a/README.md b/README.md index 7f2875cdf..5156b4e70 100644 --- a/README.md +++ b/README.md @@ -246,9 +246,16 @@ Internally Pg always uses the nonblocking connection mode of libpq. It then behaves like running in blocking mode but ensures, that all blocking IO is handled in Ruby through a possibly registered `Fiber.scheduler`. When `PG::Connection#setnonblocking(true)` is called then the nonblocking state stays enabled, but the additional handling of blocking states is disabled, so that the calling program has to handle blocking states on its own. -An exception to this rule are the methods for large objects like `PG::Connection#lo_create` and authentication methods using external libraries (like GSSAPI authentication). -They are not compatible with `Fiber.scheduler`, so that blocking states are not passed to the registered IO scheduler. -That means the operation will work properly, but IO waiting states can not be used to switch to another Fiber doing IO. +There are some exceptions to the `Fiber.scheduler` compatibility, so that blocking states are not passed to the registered IO scheduler. +That means the operation will work properly, but IO waiting states can not be used to switch to another Fiber doing IO: + +* Methods for large objects like `PG::Connection#lo_create` +* Authentication methods using external libraries (like GSSAPI or LDAP authentication) +* LDAP Lookup of Connection Parameters: https://www.postgresql.org/docs/current/libpq-ldap.html +* A connection string/hash with the `service` parameter set but not `host` and `port`. + `Thread.scheduler` compatible alternatives are: + * Set the service via `PGSERVICE`environment variable + * Set the `host` and `port` parameters explicit in the connection string/hash. ## Ractor support diff --git a/lib/pg/connection.rb b/lib/pg/connection.rb index 09e23b50c..5adabe108 100644 --- a/lib/pg/connection.rb +++ b/lib/pg/connection.rb @@ -589,7 +589,9 @@ def reset # Use connection options from PG::Connection.new to reconnect with the same options but with renewed DNS resolution. # Use conninfo_hash as a fallback when connect_start was used to create the connection object. iopts = @iopts_for_reset || conninfo_hash.compact - if iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000 + if iopts[:host] && !iopts[:host].empty? && + iopts[:port] && !iopts[:port].empty? && + PG.library_version >= 100000 iopts = self.class.send(:resolve_hosts, iopts) end conninfo = self.class.parse_connect_args( iopts ); @@ -916,13 +918,24 @@ def new(*args) port: dests.map{|d| d[2] }.join(",")) end + RESOLUTION_KEYS = [:host, :hostaddr, :port].freeze + private_constant :RESOLUTION_KEYS + private def connect_to_hosts(*args) option_string = parse_connect_args(*args) - iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] } - iopts = PG::Connection.conndefaults.each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] }.merge(iopts) + iopts = PG::Connection.conninfo_parse(option_string).each_with_object({}) { |h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] } + + has_explicit_host = (!iopts[:host].to_s.empty? || !iopts[:hostaddr].to_s.empty?) && !iopts[:port].to_s.empty? + has_explicit_service = !iopts[:service].to_s.empty? - if PG::BUNDLED_LIBPQ_WITH_UNIXSOCKET && iopts[:host].to_s.empty? && iopts[:hostaddr].to_s.empty? - # Many distors patch the hardcoded default UnixSocket path in libpq to /var/run/postgresql instead of /tmp . + iopts_with_defaults = PG::Connection.conndefaults.each_with_object({}) do |h, o| + k = h[:keyword].to_sym + # Only use the host/hostname/port keys from defaults and leave user/dbname/sslmode/... for libpq, so that they are processed in the right order. + o[k] = h[:val] if h[:val] && RESOLUTION_KEYS.include?(k) + end.merge(iopts) + + if PG::BUNDLED_LIBPQ_WITH_UNIXSOCKET && iopts_with_defaults[:host].to_s.empty? && iopts_with_defaults[:hostaddr].to_s.empty? + # Many distros patch the hardcoded default UnixSocket path in libpq to /var/run/postgresql instead of /tmp . # We simply try them all. iopts[:host] = "/var/run/postgresql" + # Ubuntu, Debian, Fedora, Opensuse ",/run/postgresql" + # Alpine, Archlinux, Gentoo @@ -930,15 +943,22 @@ def new(*args) end iopts_for_reset = iopts - if iopts[:hostaddr] + if iopts_with_defaults[:hostaddr] # hostaddr is provided -> no need to resolve hostnames - - elsif iopts[:host] && !iopts[:host].empty? && PG.library_version >= 100000 - iopts = resolve_hosts(iopts) + elsif has_explicit_service && !has_explicit_host + # The pg_service.conf might provide host/user/port/etc. + # Ruby-pg cannot interpret pg_service without reading INI file and the additional LDAP stuff. + # So, pass params through and let libpq resolve the service, possibly blocking the Thread.scheduler. + # This ensures the processing order of libpq which is: + # connection string => service file => environment variable => compiled default + elsif iopts_with_defaults[:host] && !iopts_with_defaults[:host].empty? && PG.library_version >= 100000 + # Do host resolution to avoid blocking Thread.scheduler while DNS queries. + iopts_for_reset = iopts_with_defaults + iopts = resolve_hosts(iopts_with_defaults) else # No host given end - conn = self.connect_start(iopts) or + conn = connect_start(iopts) or raise(PG::Error, "Unable to create a new connection") raise PG::ConnectionBad, conn.error_message if conn.status == PG::CONNECTION_BAD diff --git a/spec/helpers.rb b/spec/helpers.rb index e974def5e..bba0eeb86 100644 --- a/spec/helpers.rb +++ b/spec/helpers.rb @@ -34,6 +34,7 @@ def self::included( mod ) @conninfo = $pg_server.conninfo @unix_socket = $pg_server.unix_socket @conn = $pg_server.connect + @user = ENV['USER'] || ENV['USERNAME'] # Find a local port that is not in use @port_down = @port + 10 @@ -194,6 +195,7 @@ class PostgresServer attr_reader :port attr_reader :conninfo attr_reader :unix_socket + attr_reader :test_dir ### Set up a PostgreSQL database instance for testing. def initialize(name, port: 23456, postgresql_conf: '') diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index 1546344f7..cdbb91afd 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -30,34 +30,36 @@ expect( c.finished? ).to be_falsey end - it "shouldn't be shareable for Ractor", :ractor do - c = PG.connect(@conninfo) - expect{ Ractor.make_shareable(c) }.to raise_error(Ractor::Error, /PG::Connection/) - ensure - c&.finish - end - - it "should be usable with Ractor", :ractor do - vals = Ractor.new(@conninfo) do |conninfo| - conn = PG.connect(conninfo) - conn.setnonblocking true - conn.setnonblocking false - conn.exec("SELECT 123").values + context "Ractor", :ractor do + it "shouldn't be shareable for Ractor" do + c = PG.connect(@conninfo) + expect{ Ractor.make_shareable(c) }.to raise_error(Ractor::Error, /PG::Connection/) ensure - conn&.finish - end.value + c&.finish + end - expect( vals ).to eq( [["123"]] ) - end + it "should be usable with Ractor" do + vals = Ractor.new(@conninfo) do |conninfo| + conn = PG.connect(conninfo) + conn.setnonblocking true + conn.setnonblocking false + conn.exec("SELECT 123").values + ensure + conn&.finish + end.value - it "connects using 7 arguments in a Ractor", :ractor do - vals = Ractor.new(@port) do |port| - PG.connect( 'localhost', port, nil, nil, :test, nil, nil ) do |conn| - conn.exec("SELECT 234").values - end - end.value + expect( vals ).to eq( [["123"]] ) + end + + it "connects using 7 arguments in a Ractor" do + vals = Ractor.new(@port) do |port| + PG.connect( 'localhost', port, nil, nil, :test, nil, nil ) do |conn| + conn.exec("SELECT 234").values + end + end.value - expect( vals ).to eq( [["234"]] ) + expect( vals ).to eq( [["234"]] ) + end end describe "#inspect", :without_transaction do @@ -315,273 +317,293 @@ end end - it "connects successfully with connection string" do - tmpconn = described_class.connect( @conninfo ) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - tmpconn.finish - end + context "connect" do + it "successfully with connection string" do + tmpconn = described_class.connect( @conninfo ) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + tmpconn.finish + end - it "connects using 7 arguments converted to strings" do - tmpconn = described_class.connect( 'localhost', @port, nil, nil, :test, nil, nil ) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - tmpconn.finish - end + it "using 7 arguments converted to strings" do + tmpconn = described_class.connect( 'localhost', @port, nil, nil, :test, nil, nil ) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + tmpconn.finish + end - it "connects using a hash of connection parameters" do - tmpconn = described_class.connect( - :host => 'localhost', - :port => @port, - :dbname => :test) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - tmpconn.finish - end + it "using a hash of connection parameters" do + tmpconn = described_class.connect( + :host => 'localhost', + :port => @port, + :dbname => :test) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + tmpconn.finish + end - it "connects using a hash of optional connection parameters" do - tmpconn = described_class.connect( - :host => 'localhost', - :port => @port, - :dbname => :test, - :keepalives => 1) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - tmpconn.finish - end + it "using a hash of optional connection parameters" do + tmpconn = described_class.connect( + :host => 'localhost', + :port => @port, + :dbname => :test, + :keepalives => 1) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + tmpconn.finish + end - it "raises an exception when connecting with an invalid number of arguments" do - expect { - described_class.connect( 1, 2, 3, 4, 5, 6, 7, 'the-extra-arg' ) - }.to raise_error do |error| - expect( error ).to be_an( ArgumentError ) - expect( error.message ).to match( /extra positional parameter/i ) - expect( error.message ).to match( /8/ ) - expect( error.message ).to match( /the-extra-arg/ ) + it "raises an exception when connecting with an invalid number of arguments" do + expect { + described_class.connect( 1, 2, 3, 4, 5, 6, 7, 'the-extra-arg' ) + }.to raise_error do |error| + expect( error ).to be_an( ArgumentError ) + expect( error.message ).to match( /extra positional parameter/i ) + expect( error.message ).to match( /8/ ) + expect( error.message ).to match( /the-extra-arg/ ) + end end - end - it "emits a suitable error_message at connection errors" do - skip("Will be fixed in postgresql-15 on Windows") if RUBY_PLATFORM=~/mingw|mswin/ + it "emits a suitable error_message at connection errors" do + skip("Will be fixed in postgresql-15 on Windows") if RUBY_PLATFORM=~/mingw|mswin/ - expect { - described_class.connect( - :host => 'localhost', - :port => @port, - :dbname => "non-existent") - }.to raise_error do |error| - expect( error ).to be_an( PG::ConnectionBad ) - expect( error.message ).to match( /database "non-existent" does not exist/i ) - expect( error.message.encoding ).to eq( Encoding::BINARY ) + expect { + described_class.connect( + :host => 'localhost', + :port => @port, + :dbname => "non-existent") + }.to raise_error do |error| + expect( error ).to be_an( PG::ConnectionBad ) + expect( error.message ).to match( /database "non-existent" does not exist/i ) + expect( error.message.encoding ).to eq( Encoding::BINARY ) + end + end + + it "raises after 'timeout' and two times 'connection refused'" do + with_env_vars(PGHOST: nil) do + PG::TestingHelpers::ListenSocket.new do |sock| + start_time = Time.now + expect { + described_class.connect( + hostaddr: '127.0.0.1,127.0.0.1,127.0.0.1', + port: "#{@port_down},#{sock.port},#{@port_down}", + connect_timeout: RUBY_PLATFORM=~/mingw|mswin/i ? 5 : 1, + dbname: "test") + }.to raise_error do |error| + expect( error ).to be_an( PG::ConnectionBad ) + if PG.library_version >= 140000 + expect( error.message ).to match( /127\.0\.0\.1.+#{@port_down}.+(Connection refused|ECONNREFUSED).+127\.0\.0\.1.+#{sock.port}.+timeout expired.+127\.0\.0\.1.+#{@port_down}.+(Connection refused|ECONNREFUSED)/im ) + end + end + + expect( Time.now - start_time ).to be_between(0.9, 20).inclusive + end + end end - end - it "raises after 'timeout' and two times 'connection refused'" do - with_env_vars(PGHOST: nil) do + it "times out after 2 * connect_timeout seconds on two connections" do PG::TestingHelpers::ListenSocket.new do |sock| start_time = Time.now expect { described_class.connect( - hostaddr: '127.0.0.1,127.0.0.1,127.0.0.1', - port: "#{@port_down},#{sock.port},#{@port_down}", - connect_timeout: RUBY_PLATFORM=~/mingw|mswin/i ? 5 : 1, + host: '127.0.0.1,localhost', + port: sock.port, + connect_timeout: RUBY_PLATFORM=~/mingw|mswin/i ? 3 : 1, dbname: "test") }.to raise_error do |error| expect( error ).to be_an( PG::ConnectionBad ) if PG.library_version >= 140000 - expect( error.message ).to match( /127\.0\.0\.1.+#{@port_down}.+(Connection refused|ECONNREFUSED).+127\.0\.0\.1.+#{sock.port}.+timeout expired.+127\.0\.0\.1.+#{@port_down}.+(Connection refused|ECONNREFUSED)/im ) + expect( error.message ).to match( /127\.0\.0\.1.+#{sock.port}.+timeout expired.+127\.0\.0\.1.+#{sock.port}.+timeout expired/im ) end end - expect( Time.now - start_time ).to be_between(0.9, 20).inclusive + expect( Time.now - start_time ).to be_between(1.9, 20).inclusive end end - end - it "times out after 2 * connect_timeout seconds on two connections" do - PG::TestingHelpers::ListenSocket.new do |sock| - start_time = Time.now - expect { - described_class.connect( - host: '127.0.0.1,localhost', - port: sock.port, - connect_timeout: RUBY_PLATFORM=~/mingw|mswin/i ? 3 : 1, + it "succeeds with second host after connect_timeout" do + PG::TestingHelpers::ListenSocket.new do |sock| + start_time = Time.now + conn = described_class.connect( + host: 'localhost,localhost,localhost', + port: "#{sock.port},#{@port},#{sock.port}", + connect_timeout: 1, dbname: "test") - }.to raise_error do |error| - expect( error ).to be_an( PG::ConnectionBad ) - if PG.library_version >= 140000 - expect( error.message ).to match( /127\.0\.0\.1.+#{sock.port}.+timeout expired.+127\.0\.0\.1.+#{sock.port}.+timeout expired/im ) - end - end - - expect( Time.now - start_time ).to be_between(1.9, 20).inclusive - end - end - it "succeeds with second host after connect_timeout" do - PG::TestingHelpers::ListenSocket.new do |sock| - start_time = Time.now - conn = described_class.connect( - host: 'localhost,localhost,localhost', - port: "#{sock.port},#{@port},#{sock.port}", - connect_timeout: 1, - dbname: "test") - - expect( conn.port ).to eq( @port ) - expect( Time.now - start_time ).to be_between(0.9, 10).inclusive - ensure - conn&.finish + expect( conn.port ).to eq( @port ) + expect( Time.now - start_time ).to be_between(0.9, 10).inclusive + ensure + conn&.finish + end end - end - it "can emit messages in PQsend*, https://github.com/ged/ruby-pg/issues/171", :without_transaction do - port_ro = @port + 2 - @dbms = PG::TestingHelpers::PostgresServer.new("stopping", port: port_ro) - pg = PG.connect( host: @dbms.unix_socket, port: port_ro, dbname: "postgres" ) - pg.set_notice_processor {} - pg.prepare('time_now', 'SELECT current_timestamp') + 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 - # Server close emits a message: - # "FATAL: terminating connection due to administrator command\n" - @dbms.teardown + after :all do + @dbms&.teardown + end - # libpq version <= 11 emits messages while PQsend* functions. - # This crashed on ruby <= 3.3 due to locking GVL which is already locked. - pg.send_query_prepared('time_now') - pg.get_result - rescue PG::UnableToSend, PG::ConnectionBad - # OK, didn't crash - end + it "honors target_session_attrs requirements" 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 - 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" - ) + 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 - after :all do - @dbms&.teardown - end + it "stops hosts iteration on authentication errors", :without_transaction, :ipv6 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 returned. + /authenti.*testusermd5|Connection refused|server closed the connection unexpectedly/i + else + /authenti.*testusermd5/i + end + expect { PG.connect(uri) }.to raise_error(error_match) - it "honors target_session_attrs requirements" do - uri = "postgres://localhost:#{@port_ro},localhost:#{@port}/postgres?target_session_attrs=read-write" + 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 = "postgres://localhost:#{@port_ro},localhost:#{@port}/postgres?target_session_attrs=any" + 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.port ).to eq( @port_ro ) + expect( conn.host ).to eq( "127.0.0.1" ) + expect( conn.port ).to eq( @port ) end end - end - it "stops hosts iteration on authentication errors", :without_transaction, :ipv6 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 returned. - /authenti.*testusermd5|Connection refused|server closed the connection unexpectedly/i - else - /authenti.*testusermd5/i + it "using URI with multiple hosts", :postgresql_12 do + 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 ) + expect( tmpconn.host ).to eq( "127.0.0.1" ) + expect( tmpconn.hostaddr ).to match( /\A(::1|127\.0\.0\.1)\z/ ) + tmpconn.finish 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 ) + it "using URI with IPv6 hosts", :postgresql_12, :ipv6 do + uri = "postgres://localhost:#{@port},[::1]:#{@port},/test" + tmpconn = described_class.connect( uri ) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.host ).to eq( "localhost" ) + expect( tmpconn.hostaddr ).to match( /\A(::1|127\.0\.0\.1)\z/ ) + tmpconn.finish 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 ) + it "using URI with UnixSocket host", :postgresql_12, :unix_socket do + uri = "postgres://#{@unix_socket.gsub("/", "%2F")}:#{@port}/test" + tmpconn = described_class.connect( uri ) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.host ).to eq( @unix_socket ) + expect( tmpconn.hostaddr ).to eq( "" ) + tmpconn.finish end - end - - it "connects using URI with multiple hosts", :postgresql_12 do - 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 ) - expect( tmpconn.host ).to eq( "127.0.0.1" ) - expect( tmpconn.hostaddr ).to match( /\A(::1|127\.0\.0\.1)\z/ ) - tmpconn.finish - end - - it "connects using URI with IPv6 hosts", :postgresql_12, :ipv6 do - uri = "postgres://localhost:#{@port},[::1]:#{@port},/test" - tmpconn = described_class.connect( uri ) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - expect( tmpconn.host ).to eq( "localhost" ) - expect( tmpconn.hostaddr ).to match( /\A(::1|127\.0\.0\.1)\z/ ) - tmpconn.finish - end - - it "connects using URI with UnixSocket host", :postgresql_12, :unix_socket do - uri = "postgres://#{@unix_socket.gsub("/", "%2F")}:#{@port}/test" - tmpconn = described_class.connect( uri ) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - expect( tmpconn.host ).to eq( @unix_socket ) - expect( tmpconn.hostaddr ).to eq( "" ) - tmpconn.finish - end - it "connects with environment variables" do - skip("Is broken before postgresql-12 on Windows") if RUBY_PLATFORM=~/mingw|mswin/ && PG.library_version < 120000 + it "with environment variables" do + skip("Is broken before postgresql-12 on Windows") if RUBY_PLATFORM=~/mingw|mswin/ && PG.library_version < 120000 - tmpconn = with_env_vars(PGHOST: "localhost", PGPORT: @port, PGDATABASE: "test") do - described_class.connect + tmpconn = with_env_vars(PGHOST: "localhost", PGPORT: @port, PGDATABASE: "test") do + described_class.connect + end + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.host ).to eq( "localhost" ) + tmpconn.finish end - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - expect( tmpconn.host ).to eq( "localhost" ) - tmpconn.finish - end - - it "connects using Hash with multiple hosts", :postgresql_12 do - tmpconn = described_class.connect( host: "#{@unix_socket}xx,127.0.0.1,localhost", port: @port, dbname: "test" ) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - expect( tmpconn.host ).to eq( "127.0.0.1" ) - expect( tmpconn.hostaddr ).to match( /\A127\.0\.0\.1\z/ ) - tmpconn.finish - end - %i[open new connect sync_connect async_connect setdb setdblogin].each do |meth| - it "can call ##{meth} of a derived class" do - klass = Class.new(described_class) do - alias execute exec + it "via pg_service file and prefers explicit user,port over service over PGUSER,PGPORT" do + serfile = $pg_server.test_dir + "pg_service.conf" + File.write(serfile, "[mydb]\nuser=invalid\nport=#{@port_down}") + tmpconn = with_env_vars(PGSERVICEFILE: serfile, PGUSER: "invalid", PGPORT: @port_down) do + c = PG.connect service: "mydb", host: "localhost", user: @user, port: @port, dbname: "test" + c.reset end - klass.send(meth, @conninfo) do |conn| - expect( conn ).to be_a_kind_of( klass ) - expect( conn.execute("SELECT 1") ).to be_a_kind_of( PG::Result ) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.conninfo_hash[:service] ).to eq( "mydb" ) if PG.library_version >= 150000 + expect( tmpconn.port ).to eq( @port ) + expect( tmpconn.user ).to eq( @user ) + expect( tmpconn.port ).to eq( @port ) + tmpconn.finish + end + + [ + {}, + {host: "localhost"}, + {hostaddr: "127.0.0.1"}, + ].each do |conn_hash| + it "via #{conn_hash} and pg_service file and prefers service user,port over PGUSER,PGPORT" do + serfile = $pg_server.test_dir + "pg_service.conf" + File.write(serfile, "[mydb]\nuser=#{@user}\n#{ @conninfo.gsub(" ", "\n") }") + tmpconn = with_env_vars(PGSERVICEFILE: serfile, PGUSER: "invalid", PGPORT: @port_down) do + c = PG.connect service: "mydb", **conn_hash + c.reset + end + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.conninfo_hash[:service] ).to eq( "mydb" ) if PG.library_version >= 150000 + expect( tmpconn.port ).to eq( @port ) + expect( tmpconn.user ).to eq( @user ) + tmpconn.finish end end - end - it "can connect asynchronously" do - tmpconn = described_class.connect_start( @conninfo ) - expect( tmpconn ).to be_a( described_class ) + it "using Hash with multiple hosts", :postgresql_12 do + tmpconn = described_class.connect( host: "#{@unix_socket}xx,127.0.0.1,localhost", port: @port, dbname: "test" ) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.host ).to eq( "127.0.0.1" ) + expect( tmpconn.hostaddr ).to match( /\A127\.0\.0\.1\z/ ) + tmpconn.finish + end - wait_for_polling_ok(tmpconn) - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - tmpconn.finish + %i[open new connect sync_connect async_connect setdb setdblogin].each do |meth| + it "via ##{meth} of a derived class" do + klass = Class.new(described_class) do + alias myexecute exec + end + klass.send(meth, @conninfo) do |conn| + expect( conn ).to be_a_kind_of( klass ) + expect( conn.myexecute("SELECT 1") ).to be_a_kind_of( PG::Result ) + end + end + end end - it "can connect asynchronously for the duration of a block" do - conn = nil - - described_class.connect_start(@conninfo) do |tmpconn| + context "connect_start" do + it "can connect asynchronously" do + tmpconn = described_class.connect_start( @conninfo ) expect( tmpconn ).to be_a( described_class ) - conn = tmpconn wait_for_polling_ok(tmpconn) expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + tmpconn.finish end - expect( conn ).to be_finished() + it "can connect asynchronously for the duration of a block" do + conn = nil + + described_class.connect_start(@conninfo) do |tmpconn| + expect( tmpconn ).to be_a( described_class ) + conn = tmpconn + + wait_for_polling_ok(tmpconn) + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + end + + expect( conn ).to be_finished() + end end context "with async established connection" do @@ -660,6 +682,25 @@ end end + it "can emit messages in PQsend*, https://github.com/ged/ruby-pg/issues/171", :without_transaction do + port_ro = @port + 2 + @dbms = PG::TestingHelpers::PostgresServer.new("stopping", port: port_ro) + pg = PG.connect( host: @dbms.unix_socket, port: port_ro, dbname: "postgres" ) + pg.set_notice_processor {} + pg.prepare('time_now', 'SELECT current_timestamp') + + # Server close emits a message: + # "FATAL: terminating connection due to administrator command\n" + @dbms.teardown + + # libpq version <= 11 emits messages while PQsend* functions. + # This crashed on ruby <= 3.3 due to locking GVL which is already locked. + pg.send_query_prepared('time_now') + pg.get_result + rescue PG::UnableToSend, PG::ConnectionBad + # OK, didn't crash + end + it "can use conn.reset_start to restart the connection" do ios = IO.pipe conn = described_class.connect_start( @conninfo ) diff --git a/spec/pg/scheduler_spec.rb b/spec/pg/scheduler_spec.rb index 408ff89ea..d56caa71f 100644 --- a/spec/pg/scheduler_spec.rb +++ b/spec/pg/scheduler_spec.rb @@ -47,42 +47,65 @@ end end - it "connects several times concurrently" do - run_with_scheduler do - q = Queue.new - 3.times do - Fiber.schedule do - conn = PG.connect(@conninfo_gate) - conn.finish - q << true + context :connect do + it "connects several times concurrently" do + run_with_scheduler do + q = Queue.new + 3.times do + Fiber.schedule do + conn = PG.connect(@conninfo_gate) + conn.finish + q << true + end + end.times do + q.pop end - end.times do - q.pop end end - end - it "connects with environment variables", :postgresql_12, :unix_socket do - skip "requires ruby-3.2" if RUBY_VERSION < "3.2" - run_with_scheduler do - vars = PG::Connection.conninfo_parse(@conninfo_gate).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] } + it "connects with environment variables", :postgresql_12, :unix_socket do + skip "requires ruby-3.2" if RUBY_VERSION < "3.2" + run_with_scheduler do + vars = PG::Connection.conninfo_parse(@conninfo_gate).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] } - tmpconn = with_env_vars(PGHOST: "scheduler-localhost", PGPORT: vars[:port], PGDATABASE: vars[:dbname], PGSSLMODE: vars[:sslmode]) do - PG.connect + tmpconn = with_env_vars(PGHOST: "scheduler-localhost", PGPORT: vars[:port], PGDATABASE: vars[:dbname], PGSSLMODE: vars[:sslmode]) do + PG.connect + end + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.host ).to eq( "scheduler-localhost" ) + tmpconn.finish end - expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) - expect( tmpconn.host ).to eq( "scheduler-localhost" ) - tmpconn.finish end - end - it "can connect with DNS lookup", :scheduler_address_resolve do - run_with_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 start_with( 'scheduler-localhost' ) - conn.finish + [ + [{PGSERVICE: "mydb"}, {}], + [{PGHOTS: "ignored", PGPORT: "12345"}, { service: 'mydb', host: "scheduler-localhost" }], + ].each do |env_hash, conn_hash| + it "connects with #{env_hash.merge(conn_hash)}", :scheduler_address_resolve do + run_with_scheduler do + vars = PG::Connection.conninfo_parse(@conninfo_gate).each_with_object({}){|h, o| o[h[:keyword].to_sym] = h[:val] if h[:val] } + + serfile = $pg_server.test_dir + "pg_service.conf" + ser_hash = { host: "scheduler-localhost", port: vars[:port], dbname: vars[:dbname], sslmode: vars[:sslmode] }.compact + File.write(serfile, "[mydb]\n#{ser_hash.map{|k,v| "#{k}=#{v}" }.join("\n") }") + tmpconn = with_env_vars(**env_hash, PGSERVICEFILE: serfile) do + PG.connect port: vars[:port], **conn_hash + end + expect( tmpconn.status ).to eq( PG::CONNECTION_OK ) + expect( tmpconn.host ).to eq( "scheduler-localhost" ) + tmpconn.finish + end + end + end + + it "can connect with DNS lookup", :scheduler_address_resolve do + run_with_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 start_with( 'scheduler-localhost' ) + conn.finish + end end end