123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- # frozen_string_literal: true
- require_relative 'spec_helper'
- describe "Miscellaneous" do
- let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5) }
- after do
- processes.all_databases.map(&:reset)
- processes.pgcat.shutdown
- end
- context "when adding then removing instance using RELOAD" do
- it "works correctly" do
- admin_conn = PG::connect(processes.pgcat.admin_connection_string)
- current_configs = processes.pgcat.current_config
- correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count
- expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count)
- extra_replica = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].last.clone
- extra_replica[0] = "127.0.0.1"
- current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"] << extra_replica
- processes.pgcat.update_config(current_configs) # with replica added
- processes.pgcat.reload_config
- correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count
- expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count)
- current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].pop
- processes.pgcat.update_config(current_configs) # with replica removed again
- processes.pgcat.reload_config
- correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count
- expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count)
- end
- end
- context "when removing then adding instance back using RELOAD" do
- it "works correctly" do
- admin_conn = PG::connect(processes.pgcat.admin_connection_string)
- current_configs = processes.pgcat.current_config
- correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count
- expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count)
- removed_replica = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].pop
- processes.pgcat.update_config(current_configs) # with replica removed
- processes.pgcat.reload_config
- correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count
- expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count)
- current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"] << removed_replica
- processes.pgcat.update_config(current_configs) # with replica added again
- processes.pgcat.reload_config
- correct_count = current_configs["pools"]["sharded_db"]["shards"]["0"]["servers"].count
- expect(admin_conn.async_exec("SHOW DATABASES").count).to eq(correct_count)
- end
- end
- describe "TCP Keepalives" do
- # Ideally, we should block TCP traffic to the database using
- # iptables to mimic passive (connection is dropped without a RST packet)
- # but we cannot do this in CircleCI because iptables requires NET_ADMIN
- # capability that we cannot enable in CircleCI
- # Toxiproxy won't work either because it does not block keepalives
- # so our best bet is to query the OS keepalive params set on the socket
- context "default settings" do
- it "applies default keepalive settings" do
- # We query ss command to verify that we have correct keepalive values set
- # we can only verify the keepalives_idle parameter but that's good enough
- # example output
- #Recv-Q Send-Q Local Address:Port Peer Address:Port Process
- #0 0 127.0.0.1:60526 127.0.0.1:18432 timer:(keepalive,1min59sec,0)
- #0 0 127.0.0.1:60664 127.0.0.1:19432 timer:(keepalive,4.123ms,0)
- port_search_criteria = processes.all_databases.map { |d| "dport = :#{d.port}"}.join(" or ")
- results = `ss -t4 state established -o -at '( #{port_search_criteria} )'`.lines
- results.shift
- results.each { |line| expect(line).to match(/timer:\(keepalive,.*ms,0\)/) }
- end
- end
- context "changed settings" do
- it "applies keepalive settings from config" do
- new_configs = processes.pgcat.current_config
- new_configs["general"]["tcp_keepalives_idle"] = 120
- new_configs["general"]["tcp_keepalives_count"] = 1
- new_configs["general"]["tcp_keepalives_interval"] = 1
- processes.pgcat.update_config(new_configs)
- # We need to kill the old process that was using the default configs
- processes.pgcat.stop
- processes.pgcat.start
- processes.pgcat.wait_until_ready
- port_search_criteria = processes.all_databases.map { |d| "dport = :#{d.port}"}.join(" or ")
- results = `ss -t4 state established -o -at '( #{port_search_criteria} )'`.lines
- results.shift
- results.each { |line| expect(line).to include("timer:(keepalive,1min") }
- end
- end
- end
- describe "Extended Protocol handling" do
- it "does not send packets that client does not expect during extended protocol sequence" do
- new_configs = processes.pgcat.current_config
- new_configs["general"]["connect_timeout"] = 500
- new_configs["general"]["ban_time"] = 1
- new_configs["general"]["shutdown_timeout"] = 1
- new_configs["pools"]["sharded_db"]["users"]["0"]["pool_size"] = 1
- processes.pgcat.update_config(new_configs)
- processes.pgcat.reload_config
- 25.times do
- Thread.new do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SELECT pg_sleep(5)") rescue PG::SystemError
- ensure
- conn&.close
- end
- end
- sleep(0.5)
- conn_under_test = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- stdout, stderr = with_captured_stdout_stderr do
- 15.times do |i|
- conn_under_test.async_exec("SELECT 1") rescue PG::SystemError
- conn_under_test.exec_params("SELECT #{i} + $1", [i]) rescue PG::SystemError
- sleep 1
- end
- end
- raise StandardError, "Libpq got unexpected messages while idle" if stderr.include?("arrived from server while idle")
- end
- end
- describe "Pool recycling after config reload" do
- let(:processes) { Helpers::Pgcat.three_shard_setup("sharded_db", 5) }
- it "should update pools for new clients and clients that are no longer in transaction" do
- server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- server_conn.async_exec("BEGIN")
- # No config change yet, client should set old configs
- current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"]
- expect(current_datebase_from_pg).to eq('shard0')
- # Swap shards
- new_config = processes.pgcat.current_config
- shard0 = new_config["pools"]["sharded_db"]["shards"]["0"]
- shard1 = new_config["pools"]["sharded_db"]["shards"]["1"]
- new_config["pools"]["sharded_db"]["shards"]["0"] = shard1
- new_config["pools"]["sharded_db"]["shards"]["1"] = shard0
- # Reload config
- processes.pgcat.update_config(new_config)
- processes.pgcat.reload_config
- sleep 0.5
- # Config changed but transaction is in progress, client should set old configs
- current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"]
- expect(current_datebase_from_pg).to eq('shard0')
- server_conn.async_exec("COMMIT")
- # Transaction finished, client should get new configs
- current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"]
- expect(current_datebase_from_pg).to eq('shard1')
- # New connection should get new configs
- server_conn.close()
- server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- current_datebase_from_pg = server_conn.async_exec("SELECT current_database();")[0]["current_database"]
- expect(current_datebase_from_pg).to eq('shard1')
- end
- end
- describe "Clients closing connection in the middle of transaction" do
- it "sends a rollback to the server" do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("BEGIN")
- conn.close
- expect(processes.primary.count_query("ROLLBACK")).to eq(1)
- end
- end
- describe "Server version reporting" do
- it "reports correct version for normal and admin databases" do
- server_conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- expect(server_conn.server_version).not_to eq(0)
- server_conn.close
- admin_conn = PG::connect(processes.pgcat.admin_connection_string)
- expect(admin_conn.server_version).not_to eq(0)
- admin_conn.close
- end
- end
- describe "State clearance" do
- context "session mode" do
- let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "session") }
- it "Clears state before connection checkin" do
- # Both modes of operation should not raise
- # ERROR: prepared statement "prepared_q" already exists
- 15.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("PREPARE prepared_q (int) AS SELECT $1")
- conn.close
- end
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- initial_value = conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]
- conn.async_exec("SET statement_timeout to 1000")
- current_value = conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]
- expect(conn.async_exec("SHOW statement_timeout")[0]["statement_timeout"]).to eq("1s")
- conn.close
- end
- it "Does not send DISCARD ALL unless necessary" do
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("SELECT 1")
- conn.close
- end
- expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("SELECT 1")
- conn.async_exec("SET statement_timeout to 5000")
- conn.close
- end
- expect(processes.primary.count_query("DISCARD ALL")).to eq(10)
- end
- it "Resets server roles correctly" do
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("SELECT 1")
- conn.async_exec("SET statement_timeout to 5000")
- conn.close
- end
- expect(processes.primary.count_query("RESET ROLE")).to eq(10)
- end
- end
- context "transaction mode" do
- let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "transaction") }
- it "Clears state before connection checkin" do
- # Both modes of operation should not raise
- # ERROR: prepared statement "prepared_q" already exists
- 15.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("PREPARE prepared_q (int) AS SELECT $1")
- conn.close
- end
- 15.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.prepare("prepared_q", "SELECT $1")
- conn.close
- end
- end
- it "Does not send DISCARD ALL unless necessary" do
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("SELECT 1")
- conn.exec_params("SELECT $1", [1])
- conn.close
- end
- expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("SELECT 1")
- conn.async_exec("SET statement_timeout to 5000")
- conn.close
- end
- expect(processes.primary.count_query("DISCARD ALL")).to eq(10)
- end
- end
- context "transaction mode with transactions" do
- let(:processes) { Helpers::Pgcat.single_shard_setup("sharded_db", 5, "transaction") }
- it "Does not clear set statement state when declared in a transaction" do
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("BEGIN")
- conn.async_exec("SET statement_timeout to 1000")
- conn.async_exec("COMMIT")
- conn.close
- end
- expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
- 10.times do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("SET SERVER ROLE to 'primary'")
- conn.async_exec("BEGIN")
- conn.async_exec("SET LOCAL statement_timeout to 1000")
- conn.async_exec("COMMIT")
- conn.close
- end
- expect(processes.primary.count_query("DISCARD ALL")).to eq(0)
- end
- end
- end
- describe "Idle client timeout" do
- context "idle transaction timeout set to 0" do
- before do
- current_configs = processes.pgcat.current_config
- correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"]
- puts(current_configs["general"]["idle_client_in_transaction_timeout"])
-
- current_configs["general"]["idle_client_in_transaction_timeout"] = 0
-
- processes.pgcat.update_config(current_configs) # with timeout 0
- processes.pgcat.reload_config
- end
- it "Allow client to be idle in transaction" do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("BEGIN")
- conn.async_exec("SELECT 1")
- sleep(2)
- conn.async_exec("COMMIT")
- conn.close
- end
- end
- context "idle transaction timeout set to 500ms" do
- before do
- current_configs = processes.pgcat.current_config
- correct_idle_client_transaction_timeout = current_configs["general"]["idle_client_in_transaction_timeout"]
- current_configs["general"]["idle_client_in_transaction_timeout"] = 500
-
- processes.pgcat.update_config(current_configs) # with timeout 500
- processes.pgcat.reload_config
- end
- it "Allow client to be idle in transaction below timeout" do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("BEGIN")
- conn.async_exec("SELECT 1")
- sleep(0.4) # below 500ms
- conn.async_exec("COMMIT")
- conn.close
- end
- it "Error when client idle in transaction time exceeds timeout" do
- conn = PG::connect(processes.pgcat.connection_string("sharded_db", "sharding_user"))
- conn.async_exec("BEGIN")
- conn.async_exec("SELECT 1")
- sleep(1) # above 500ms
- expect{ conn.async_exec("COMMIT") }.to raise_error(PG::SystemError, /idle transaction timeout/)
- conn.async_exec("SELECT 1") # should be able to send another query
- conn.close
- end
- end
- end
- end
|