mirror of
https://github.com/postgresml/pgcat.git
synced 2026-03-23 01:16:30 +00:00
We identified a bug where RELOAD fails to update the pools. To reproduce you need to start at some config state, modify that state a bit, reload, revert the configs back to the original state, and reload. The last reload will fail to update the pool because PgCat "thinks" the pool state didn't change. This is because we use a HashSet to keep track of config hashes but we never remove values from it. Say we start with State A, we modify pool configs to State B and reload. Now the POOL_HASHES struct has State A and State B. Attempting to go back to State A will encounter a hashset hit which is interpreted by PgCat as "Configs are the same, no need to reload pools" We fix this by attaching a config_hash value to ConnectionPool object and we calculate that value when we create the pool. This eliminates the need for a global variable. One shortcoming here is that changing any config under one user in the pool will trigger a reload for the entire pool (which is fine I think)
124 lines
3.2 KiB
Ruby
124 lines
3.2 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 = "../../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.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
|