Files
pgcat/tests/ruby/helpers/pgcat_process.rb
Mostafa Abdelraouf aa89e357e0 PgCat Query Mirroring (#341)
This is an implementation of Query mirroring in PgCat (outlined here #302)

In configs, we match mirror hosts with the servers handling the traffic. A mirror host will receive the same protocol messages as the main server it was matched with.

This is done by creating an async task for each mirror server, it communicates with the main server through two channels, one for the protocol messages and one for the exit signal. The mirror server sends the protocol packets to the underlying PostgreSQL server. We receive from the underlying PostgreSQL server as soon as the data is available and we immediately discard it. We use bb8 to manage the life cycle of the connection, not for pooling since each mirror server handler is more or less single-threaded.

We don't have any connection pooling in the mirrors. Matching each mirror connection to an actual server connection guarantees that we will not have more connections to any of the mirrors than the parent pool would allow.
2023-03-10 06:23:51 -06:00

132 lines
3.5 KiB
Ruby

require 'pg'
require 'toml'
require 'fileutils'
require 'securerandom'
class PgcatProcess
attr_reader :port
attr_reader :pid
def self.finalize(pid, log_filename, config_filename)
if pid
Process.kill("TERM", pid)
Process.wait(pid)
end
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_path = if ENV['CARGO_TARGET_DIR'] then
"#{ENV['CARGO_TARGET_DIR']}/debug/pgcat"
else
'../../target/debug/pgcat'
end
@command = "#{command_path} #{@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,')
output_to_write = output_to_write.gsub(/,\s*["|'](\d+)["|']\s*\]/, ',\1]')
File.write(@config_filename, output_to_write)
end
def current_config
loadable_string = File.read(@config_filename)
loadable_string = loadable_string.gsub(/,\s*(\d+)\s*,/, ', "\1",')
loadable_string = loadable_string.gsub(/,\s*(\d+)\s*\]/, ', "\1"]')
TOML.load(loadable_string)
end
def reload_config
`kill -s HUP #{@pid}`
sleep 0.5
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
return unless @pid
Process.kill("TERM", @pid)
Process.wait(@pid)
@pid = nil
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}?application_name=example_app"
end
end