mirror of
https://github.com/postgresml/pgcat.git
synced 2026-03-28 03:06:29 +00:00
Ruby integration tests (#147)
* Ruby integration tests * forgot a file * refactor * refactoring * more refactoring * remove config helper * try multiple databases * fix * more databases * Use pg stats * ports * speed * Fix tests * preload library * comment
This commit is contained in:
committed by
GitHub
parent
2628dec42e
commit
d48c04a7fb
82
tests/ruby/helpers/pg_instance.rb
Normal file
82
tests/ruby/helpers/pg_instance.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
require 'pg'
|
||||
require 'toxiproxy'
|
||||
|
||||
class PgInstance
|
||||
attr_reader :port
|
||||
attr_reader :username
|
||||
attr_reader :password
|
||||
attr_reader :database_name
|
||||
|
||||
def initialize(port, username, password, database_name)
|
||||
@original_port = port
|
||||
@toxiproxy_port = 10000 + port.to_i
|
||||
@port = @toxiproxy_port
|
||||
|
||||
@username = username
|
||||
@password = password
|
||||
@database_name = database_name
|
||||
@toxiproxy_name = "database_#{@original_port}"
|
||||
Toxiproxy.populate([{
|
||||
name: @toxiproxy_name,
|
||||
listen: "0.0.0.0:#{@toxiproxy_port}",
|
||||
upstream: "localhost:#{@original_port}",
|
||||
}])
|
||||
|
||||
# Toxiproxy server will outlive our PgInstance objects
|
||||
# so we want to destroy our proxies before exiting
|
||||
# Ruby finalizer is ideal for doing this
|
||||
ObjectSpace.define_finalizer(@toxiproxy_name, proc { Toxiproxy[@toxiproxy_name].destroy })
|
||||
end
|
||||
|
||||
def with_connection
|
||||
conn = PG.connect("postgres://#{@username}:#{@password}@localhost:#{port}/#{database_name}")
|
||||
yield conn
|
||||
ensure
|
||||
conn&.close
|
||||
end
|
||||
|
||||
def reset
|
||||
reset_toxics
|
||||
reset_stats
|
||||
end
|
||||
|
||||
def toxiproxy
|
||||
Toxiproxy[@toxiproxy_name]
|
||||
end
|
||||
|
||||
def take_down
|
||||
if block_given?
|
||||
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 5).apply { yield }
|
||||
else
|
||||
Toxiproxy[@toxiproxy_name].toxic(:limit_data, bytes: 5).toxics.each(&:save)
|
||||
end
|
||||
end
|
||||
|
||||
def add_latency(latency)
|
||||
if block_given?
|
||||
Toxiproxy[@toxiproxy_name].toxic(:latency, latency: latency).apply { yield }
|
||||
else
|
||||
Toxiproxy[@toxiproxy_name].toxic(:latency, latency: latency).toxics.each(&:save)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_proxy
|
||||
Toxiproxy[@toxiproxy_name].delete
|
||||
end
|
||||
|
||||
def reset_toxics
|
||||
Toxiproxy[@toxiproxy_name].toxics.each(&:destroy)
|
||||
end
|
||||
|
||||
def reset_stats
|
||||
with_connection { |c| c.async_exec("SELECT pg_stat_statements_reset()") }
|
||||
end
|
||||
|
||||
def count_query(query)
|
||||
with_connection { |c| c.async_exec("SELECT SUM(calls) FROM pg_stat_statements WHERE query = '#{query}'")[0]["sum"].to_i }
|
||||
end
|
||||
|
||||
def count_select_1_plus_2
|
||||
with_connection { |c| c.async_exec("SELECT SUM(calls) FROM pg_stat_statements WHERE query = 'SELECT $1 + $2'")[0]["sum"].to_i }
|
||||
end
|
||||
end
|
||||
100
tests/ruby/helpers/pgcat_helper.rb
Normal file
100
tests/ruby/helpers/pgcat_helper.rb
Normal file
@@ -0,0 +1,100 @@
|
||||
require 'json'
|
||||
require 'ostruct'
|
||||
require_relative 'pgcat_process'
|
||||
require_relative 'pg_instance'
|
||||
|
||||
module Helpers
|
||||
module Pgcat
|
||||
def self.three_shard_setup(pool_name, pool_size)
|
||||
user = {
|
||||
"password" => "sharding_user",
|
||||
"pool_size" => pool_size,
|
||||
"statement_timeout" => 0,
|
||||
"username" => "sharding_user"
|
||||
}
|
||||
|
||||
pgcat = PgcatProcess.new("info")
|
||||
primary0 = PgInstance.new(5432, user["username"], user["password"], "shard0")
|
||||
primary1 = PgInstance.new(7432, user["username"], user["password"], "shard1")
|
||||
primary2 = PgInstance.new(8432, user["username"], user["password"], "shard2")
|
||||
|
||||
pgcat_cfg = pgcat.current_config
|
||||
pgcat_cfg["pools"] = {
|
||||
"#{pool_name}" => {
|
||||
"default_role" => "any",
|
||||
"pool_mode" => "transaction",
|
||||
"primary_reads_enabled" => false,
|
||||
"query_parser_enabled" => false,
|
||||
"sharding_function" => "pg_bigint_hash",
|
||||
"shards" => {
|
||||
"0" => { "database" => "shard0", "servers" => [["localhost", primary0.port.to_s, "primary"]] },
|
||||
"1" => { "database" => "shard1", "servers" => [["localhost", primary1.port.to_s, "primary"]] },
|
||||
"2" => { "database" => "shard2", "servers" => [["localhost", primary2.port.to_s, "primary"]] },
|
||||
},
|
||||
"users" => { "0" => user }
|
||||
}
|
||||
}
|
||||
pgcat.update_config(pgcat_cfg)
|
||||
|
||||
pgcat.start
|
||||
pgcat.wait_until_ready
|
||||
|
||||
OpenStruct.new.tap do |struct|
|
||||
struct.pgcat = pgcat
|
||||
struct.shards = [primary0, primary1, primary2]
|
||||
struct.all_databases = [primary0, primary1, primary2]
|
||||
end
|
||||
end
|
||||
|
||||
def self.single_shard_setup(pool_name, pool_size)
|
||||
user = {
|
||||
"password" => "sharding_user",
|
||||
"pool_size" => pool_size,
|
||||
"statement_timeout" => 0,
|
||||
"username" => "sharding_user"
|
||||
}
|
||||
|
||||
pgcat = PgcatProcess.new("info")
|
||||
pgcat_cfg = pgcat.current_config
|
||||
|
||||
primary = PgInstance.new(5432, user["username"], user["password"], "shard0")
|
||||
replica0 = PgInstance.new(7432, user["username"], user["password"], "shard0")
|
||||
replica1 = PgInstance.new(8432, user["username"], user["password"], "shard0")
|
||||
replica2 = PgInstance.new(9432, user["username"], user["password"], "shard0")
|
||||
|
||||
# Main proxy configs
|
||||
pgcat_cfg["pools"] = {
|
||||
"#{pool_name}" => {
|
||||
"default_role" => "any",
|
||||
"pool_mode" => "transaction",
|
||||
"primary_reads_enabled" => false,
|
||||
"query_parser_enabled" => false,
|
||||
"sharding_function" => "pg_bigint_hash",
|
||||
"shards" => {
|
||||
"0" => {
|
||||
"database" => "shard0",
|
||||
"servers" => [
|
||||
["localhost", primary.port.to_s, "primary"],
|
||||
["localhost", replica0.port.to_s, "replica"],
|
||||
["localhost", replica1.port.to_s, "replica"],
|
||||
["localhost", replica2.port.to_s, "replica"]
|
||||
]
|
||||
},
|
||||
},
|
||||
"users" => { "0" => user }
|
||||
}
|
||||
}
|
||||
pgcat_cfg["general"]["port"] = pgcat.port
|
||||
pgcat.update_config(pgcat_cfg)
|
||||
pgcat.start
|
||||
pgcat.wait_until_ready
|
||||
|
||||
OpenStruct.new.tap do |struct|
|
||||
struct.pgcat = pgcat
|
||||
struct.primary = primary
|
||||
struct.replicas = [replica0, replica1, replica2]
|
||||
struct.all_databases = [primary, replica0, replica1, replica2]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
116
tests/ruby/helpers/pgcat_process.rb
Normal file
116
tests/ruby/helpers/pgcat_process.rb
Normal file
@@ -0,0 +1,116 @@
|
||||
require 'pg'
|
||||
require 'toml'
|
||||
require 'fileutils'
|
||||
require 'securerandom'
|
||||
|
||||
class PgcatProcess
|
||||
attr_reader :port
|
||||
attr_reader :pid
|
||||
|
||||
def self.finalize(pid, log_filename, config_filename)
|
||||
`kill #{pid}`
|
||||
File.delete(config_filename) if File.exist?(config_filename)
|
||||
File.delete(log_filename) if File.exist?(log_filename)
|
||||
end
|
||||
|
||||
def initialize(log_level)
|
||||
@env = {"RUST_LOG" => log_level}
|
||||
@port = rand(20000..32760)
|
||||
@log_level = log_level
|
||||
@log_filename = "/tmp/pgcat_log_#{SecureRandom.urlsafe_base64}.log"
|
||||
@config_filename = "/tmp/pgcat_cfg_#{SecureRandom.urlsafe_base64}.toml"
|
||||
|
||||
@command = "../../target/debug/pgcat #{@config_filename}"
|
||||
|
||||
FileUtils.cp("../../pgcat.toml", @config_filename)
|
||||
cfg = current_config
|
||||
cfg["general"]["port"] = @port.to_i
|
||||
cfg["general"]["enable_prometheus_exporter"] = false
|
||||
|
||||
update_config(cfg)
|
||||
end
|
||||
|
||||
def logs
|
||||
File.read(@log_filename)
|
||||
end
|
||||
|
||||
def update_config(config_hash)
|
||||
@original_config = current_config
|
||||
output_to_write = TOML::Generator.new(config_hash).body
|
||||
output_to_write = output_to_write.gsub(/,\s*["|'](\d+)["|']\s*,/, ',\1,')
|
||||
File.write(@config_filename, output_to_write)
|
||||
end
|
||||
|
||||
def current_config
|
||||
old_cfg = File.read(@config_filename)
|
||||
loadable_string = old_cfg.gsub(/,\s*(\d+)\s*,/, ', "\1",')
|
||||
TOML.load(loadable_string)
|
||||
end
|
||||
|
||||
def reload_config
|
||||
`kill -s HUP #{@pid}`
|
||||
sleep 0.1
|
||||
end
|
||||
|
||||
def start
|
||||
raise StandardError, "Process is already started" unless @pid.nil?
|
||||
@pid = Process.spawn(@env, @command, err: @log_filename, out: @log_filename)
|
||||
ObjectSpace.define_finalizer(@log_filename, proc { PgcatProcess.finalize(@pid, @log_filename, @config_filename) })
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
def wait_until_ready
|
||||
exc = nil
|
||||
10.times do
|
||||
PG::connect(example_connection_string).close
|
||||
|
||||
return self
|
||||
rescue => e
|
||||
exc = e
|
||||
sleep(0.5)
|
||||
end
|
||||
puts exc
|
||||
raise StandardError, "Process #{@pid} never became ready. Logs #{logs}"
|
||||
end
|
||||
|
||||
def stop
|
||||
`kill #{@pid}`
|
||||
sleep 0.1
|
||||
end
|
||||
|
||||
def shutdown
|
||||
stop
|
||||
File.delete(@config_filename) if File.exist?(@config_filename)
|
||||
File.delete(@log_filename) if File.exist?(@log_filename)
|
||||
end
|
||||
|
||||
def admin_connection_string
|
||||
cfg = current_config
|
||||
username = cfg["general"]["admin_username"]
|
||||
password = cfg["general"]["admin_password"]
|
||||
|
||||
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/pgcat"
|
||||
end
|
||||
|
||||
def connection_string(pool_name, username)
|
||||
cfg = current_config
|
||||
|
||||
user_idx, user_obj = cfg["pools"][pool_name]["users"].detect { |k, user| user["username"] == username }
|
||||
password = user_obj["password"]
|
||||
|
||||
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/#{pool_name}"
|
||||
end
|
||||
|
||||
def example_connection_string
|
||||
cfg = current_config
|
||||
first_pool_name = cfg["pools"].keys[0]
|
||||
|
||||
db_name = first_pool_name
|
||||
|
||||
username = cfg["pools"][first_pool_name]["users"]["0"]["username"]
|
||||
password = cfg["pools"][first_pool_name]["users"]["0"]["password"]
|
||||
|
||||
"postgresql://#{username}:#{password}@0.0.0.0:#{@port}/#{db_name}"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user