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:
Mostafa Abdelraouf
2022-08-30 11:14:53 -05:00
committed by GitHub
parent 2628dec42e
commit d48c04a7fb
13 changed files with 639 additions and 165 deletions

View 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

View 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

View 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