mirror of
https://github.com/postgresml/pgcat.git
synced 2026-03-26 18:36:28 +00:00
Live reloading entire config and bug fixes (#84)
* Support reloading the entire config (including sharding logic) without restart. * Fix bug incorrectly handing error reporting when the shard is set incorrectly via SET SHARD TO command. selected wrong shard and the connection keep reporting fatal #80. * Fix total_received and avg_recv admin database statistics. * Enabling the query parser by default. * More tests.
This commit is contained in:
@@ -12,12 +12,14 @@ use crate::config::{get_config, Role};
|
||||
use crate::sharding::{Sharder, ShardingFunction};
|
||||
|
||||
/// Regexes used to parse custom commands.
|
||||
const CUSTOM_SQL_REGEXES: [&str; 5] = [
|
||||
const CUSTOM_SQL_REGEXES: [&str; 7] = [
|
||||
r"(?i)^ *SET SHARDING KEY TO '?([0-9]+)'? *;? *$",
|
||||
r"(?i)^ *SET SHARD TO '?([0-9]+|ANY)'? *;? *$",
|
||||
r"(?i)^ *SHOW SHARD *;? *$",
|
||||
r"(?i)^ *SET SERVER ROLE TO '(PRIMARY|REPLICA|ANY|AUTO|DEFAULT)' *;? *$",
|
||||
r"(?i)^ *SHOW SERVER ROLE *;? *$",
|
||||
r"(?i)^ *SET PRIMARY READS TO '?(on|off|default)'? *;? *$",
|
||||
r"(?i)^ *SHOW PRIMARY READS *;? *$",
|
||||
];
|
||||
|
||||
/// Custom commands.
|
||||
@@ -28,6 +30,8 @@ pub enum Command {
|
||||
ShowShard,
|
||||
SetServerRole,
|
||||
ShowServerRole,
|
||||
SetPrimaryReads,
|
||||
ShowPrimaryReads,
|
||||
}
|
||||
|
||||
/// Quickly test for match when a query is received.
|
||||
@@ -38,27 +42,17 @@ static CUSTOM_SQL_REGEX_LIST: OnceCell<Vec<Regex>> = OnceCell::new();
|
||||
|
||||
/// The query router.
|
||||
pub struct QueryRouter {
|
||||
/// By default, queries go here, unless we have better information
|
||||
/// about what the client wants.
|
||||
default_server_role: Option<Role>,
|
||||
|
||||
/// Number of shards in the cluster.
|
||||
shards: usize,
|
||||
|
||||
/// Which shard we should be talking to right now.
|
||||
active_shard: Option<usize>,
|
||||
|
||||
/// Which server should we be talking to.
|
||||
active_role: Option<Role>,
|
||||
|
||||
/// Include the primary into the replica pool for reads.
|
||||
primary_reads_enabled: bool,
|
||||
|
||||
/// Should we try to parse queries to route them to replicas or primary automatically.
|
||||
/// Should we try to parse queries to route them to replicas or primary automatically
|
||||
query_parser_enabled: bool,
|
||||
|
||||
/// Which sharding function we're using.
|
||||
sharding_function: ShardingFunction,
|
||||
/// Include the primary into the replica pool for reads.
|
||||
primary_reads_enabled: bool,
|
||||
}
|
||||
|
||||
impl QueryRouter {
|
||||
@@ -97,28 +91,11 @@ impl QueryRouter {
|
||||
pub fn new() -> QueryRouter {
|
||||
let config = get_config();
|
||||
|
||||
let default_server_role = match config.query_router.default_role.as_ref() {
|
||||
"any" => None,
|
||||
"primary" => Some(Role::Primary),
|
||||
"replica" => Some(Role::Replica),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let sharding_function = match config.query_router.sharding_function.as_ref() {
|
||||
"pg_bigint_hash" => ShardingFunction::PgBigintHash,
|
||||
"sha1" => ShardingFunction::Sha1,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
QueryRouter {
|
||||
default_server_role: default_server_role,
|
||||
shards: config.shards.len(),
|
||||
|
||||
active_role: default_server_role,
|
||||
active_shard: None,
|
||||
primary_reads_enabled: config.query_router.primary_reads_enabled,
|
||||
active_role: None,
|
||||
query_parser_enabled: config.query_router.query_parser_enabled,
|
||||
sharding_function,
|
||||
primary_reads_enabled: config.query_router.primary_reads_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,21 +123,48 @@ impl QueryRouter {
|
||||
|
||||
let matches: Vec<_> = regex_set.matches(&query).into_iter().collect();
|
||||
|
||||
// This is not a custom query, try to infer which
|
||||
// server it'll go to if the query parser is enabled.
|
||||
if matches.len() != 1 {
|
||||
debug!("Regular query");
|
||||
if self.query_parser_enabled && self.role() == None {
|
||||
debug!("Inferring role");
|
||||
self.infer_role(buf.clone());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let config = get_config();
|
||||
|
||||
let sharding_function = match config.query_router.sharding_function.as_ref() {
|
||||
"pg_bigint_hash" => ShardingFunction::PgBigintHash,
|
||||
"sha1" => ShardingFunction::Sha1,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let default_server_role = match config.query_router.default_role.as_ref() {
|
||||
"any" => None,
|
||||
"primary" => Some(Role::Primary),
|
||||
"replica" => Some(Role::Replica),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let command = match matches[0] {
|
||||
0 => Command::SetShardingKey,
|
||||
1 => Command::SetShard,
|
||||
2 => Command::ShowShard,
|
||||
3 => Command::SetServerRole,
|
||||
4 => Command::ShowServerRole,
|
||||
5 => Command::SetPrimaryReads,
|
||||
6 => Command::ShowPrimaryReads,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut value = match command {
|
||||
Command::SetShardingKey | Command::SetShard | Command::SetServerRole => {
|
||||
Command::SetShardingKey
|
||||
| Command::SetShard
|
||||
| Command::SetServerRole
|
||||
| Command::SetPrimaryReads => {
|
||||
// Capture value. I know this re-runs the regex engine, but I haven't
|
||||
// figured out a better way just yet. I think I can write a single Regex
|
||||
// that matches all 5 custom SQL patterns, but maybe that's not very legible?
|
||||
@@ -187,11 +191,16 @@ impl QueryRouter {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Command::ShowPrimaryReads => match self.primary_reads_enabled {
|
||||
true => String::from("on"),
|
||||
false => String::from("off"),
|
||||
},
|
||||
};
|
||||
|
||||
match command {
|
||||
Command::SetShardingKey => {
|
||||
let sharder = Sharder::new(self.shards, self.sharding_function);
|
||||
let sharder = Sharder::new(config.shards.len(), sharding_function);
|
||||
let shard = sharder.shard(value.parse::<i64>().unwrap());
|
||||
self.active_shard = Some(shard);
|
||||
value = shard.to_string();
|
||||
@@ -199,7 +208,7 @@ impl QueryRouter {
|
||||
|
||||
Command::SetShard => {
|
||||
self.active_shard = match value.to_ascii_uppercase().as_ref() {
|
||||
"ANY" => Some(rand::random::<usize>() % self.shards),
|
||||
"ANY" => Some(rand::random::<usize>() % config.shards.len()),
|
||||
_ => Some(value.parse::<usize>().unwrap()),
|
||||
};
|
||||
}
|
||||
@@ -227,8 +236,8 @@ impl QueryRouter {
|
||||
}
|
||||
|
||||
"default" => {
|
||||
self.active_role = self.default_server_role;
|
||||
self.query_parser_enabled = get_config().query_router.query_parser_enabled;
|
||||
self.active_role = default_server_role;
|
||||
self.query_parser_enabled = config.query_router.query_parser_enabled;
|
||||
self.active_role
|
||||
}
|
||||
|
||||
@@ -236,6 +245,19 @@ impl QueryRouter {
|
||||
};
|
||||
}
|
||||
|
||||
Command::SetPrimaryReads => {
|
||||
if value == "on" {
|
||||
debug!("Setting primary reads to on");
|
||||
self.primary_reads_enabled = true;
|
||||
} else if value == "off" {
|
||||
debug!("Setting primary reads to off");
|
||||
self.primary_reads_enabled = false;
|
||||
} else if value == "default" {
|
||||
debug!("Setting primary reads to default");
|
||||
self.primary_reads_enabled = config.query_router.primary_reads_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -330,23 +352,15 @@ impl QueryRouter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the router back to defaults.
|
||||
/// This must be called at the end of every transaction in transaction mode.
|
||||
pub fn _reset(&mut self) {
|
||||
self.active_role = self.default_server_role;
|
||||
self.active_shard = None;
|
||||
pub fn set_shard(&mut self, shard: usize) {
|
||||
self.active_shard = Some(shard);
|
||||
}
|
||||
|
||||
/// Should we attempt to parse queries?
|
||||
#[allow(dead_code)]
|
||||
pub fn query_parser_enabled(&self) -> bool {
|
||||
self.query_parser_enabled
|
||||
}
|
||||
|
||||
/// Allows to toggle primary reads in tests.
|
||||
#[allow(dead_code)]
|
||||
pub fn toggle_primary_reads(&mut self, value: bool) {
|
||||
self.primary_reads_enabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -369,7 +383,8 @@ mod test {
|
||||
let mut qr = QueryRouter::new();
|
||||
assert!(qr.try_execute_command(simple_query("SET SERVER ROLE TO 'auto'")) != None);
|
||||
assert_eq!(qr.query_parser_enabled(), true);
|
||||
qr.toggle_primary_reads(false);
|
||||
|
||||
assert!(qr.try_execute_command(simple_query("SET PRIMARY READS TO off")) != None);
|
||||
|
||||
let queries = vec![
|
||||
simple_query("SELECT * FROM items WHERE id = 5"),
|
||||
@@ -410,7 +425,7 @@ mod test {
|
||||
QueryRouter::setup();
|
||||
let mut qr = QueryRouter::new();
|
||||
let query = simple_query("SELECT * FROM items WHERE id = 5");
|
||||
qr.toggle_primary_reads(true);
|
||||
assert!(qr.try_execute_command(simple_query("SET PRIMARY READS TO on")) != None);
|
||||
|
||||
assert!(qr.infer_role(query));
|
||||
assert_eq!(qr.role(), None);
|
||||
@@ -421,7 +436,7 @@ mod test {
|
||||
QueryRouter::setup();
|
||||
let mut qr = QueryRouter::new();
|
||||
qr.try_execute_command(simple_query("SET SERVER ROLE TO 'auto'"));
|
||||
qr.toggle_primary_reads(false);
|
||||
assert!(qr.try_execute_command(simple_query("SET PRIMARY READS TO off")) != None);
|
||||
|
||||
let prepared_stmt = BytesMut::from(
|
||||
&b"WITH t AS (SELECT * FROM items WHERE name = $1) SELECT * FROM t WHERE id = $2\0"[..],
|
||||
@@ -450,6 +465,10 @@ mod test {
|
||||
"SET SERVER ROLE TO 'any'",
|
||||
"SET SERVER ROLE TO 'auto'",
|
||||
"SHOW SERVER ROLE",
|
||||
"SET PRIMARY READS TO 'on'",
|
||||
"SET PRIMARY READS TO 'off'",
|
||||
"SET PRIMARY READS TO 'default'",
|
||||
"SHOW PRIMARY READS",
|
||||
// Lower case
|
||||
"set sharding key to '1'",
|
||||
"set shard to '1'",
|
||||
@@ -459,9 +478,13 @@ mod test {
|
||||
"set server role to 'any'",
|
||||
"set server role to 'auto'",
|
||||
"show server role",
|
||||
"set primary reads to 'on'",
|
||||
"set primary reads to 'OFF'",
|
||||
"set primary reads to 'deFaUlt'",
|
||||
// No quotes
|
||||
"SET SHARDING KEY TO 11235",
|
||||
"SET SHARD TO 15",
|
||||
"SET PRIMARY READS TO off",
|
||||
// Spaces and semicolon
|
||||
" SET SHARDING KEY TO 11235 ; ",
|
||||
" SET SHARD TO 15; ",
|
||||
@@ -469,18 +492,23 @@ mod test {
|
||||
" SET SERVER ROLE TO 'primary'; ",
|
||||
" SET SERVER ROLE TO 'primary' ; ",
|
||||
" SET SERVER ROLE TO 'primary' ;",
|
||||
" SET PRIMARY READS TO 'off' ;",
|
||||
];
|
||||
|
||||
// Which regexes it'll match to in the list
|
||||
let matches = [
|
||||
0, 1, 2, 3, 3, 3, 3, 4, 0, 1, 2, 3, 3, 3, 3, 4, 0, 1, 0, 1, 0, 3, 3, 3,
|
||||
0, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 6, 0, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 0, 1, 5, 0, 1, 0,
|
||||
3, 3, 3, 5,
|
||||
];
|
||||
|
||||
let list = CUSTOM_SQL_REGEX_LIST.get().unwrap();
|
||||
let set = CUSTOM_SQL_REGEX_SET.get().unwrap();
|
||||
|
||||
for (i, test) in tests.iter().enumerate() {
|
||||
assert!(list[matches[i]].is_match(test));
|
||||
if !list[matches[i]].is_match(test) {
|
||||
println!("{} does not match {}", test, list[matches[i]]);
|
||||
assert!(false);
|
||||
}
|
||||
assert_eq!(set.matches(test).into_iter().collect::<Vec<_>>().len(), 1);
|
||||
}
|
||||
|
||||
@@ -549,6 +577,26 @@ mod test {
|
||||
Some((Command::ShowServerRole, String::from(*role)))
|
||||
);
|
||||
}
|
||||
|
||||
let primary_reads = ["on", "off", "default"];
|
||||
let primary_reads_enabled = ["on", "off", "on"];
|
||||
|
||||
for (idx, primary_reads) in primary_reads.iter().enumerate() {
|
||||
assert_eq!(
|
||||
qr.try_execute_command(simple_query(&format!(
|
||||
"SET PRIMARY READS TO {}",
|
||||
primary_reads
|
||||
))),
|
||||
Some((Command::SetPrimaryReads, String::from(*primary_reads)))
|
||||
);
|
||||
assert_eq!(
|
||||
qr.try_execute_command(simple_query("SHOW PRIMARY READS")),
|
||||
Some((
|
||||
Command::ShowPrimaryReads,
|
||||
String::from(primary_reads_enabled[idx])
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -556,7 +604,7 @@ mod test {
|
||||
QueryRouter::setup();
|
||||
let mut qr = QueryRouter::new();
|
||||
let query = simple_query("SET SERVER ROLE TO 'auto'");
|
||||
qr.toggle_primary_reads(false);
|
||||
assert!(qr.try_execute_command(simple_query("SET PRIMARY READS TO off")) != None);
|
||||
|
||||
assert!(qr.try_execute_command(query) != None);
|
||||
assert!(qr.query_parser_enabled());
|
||||
@@ -573,6 +621,6 @@ mod test {
|
||||
assert!(qr.query_parser_enabled());
|
||||
let query = simple_query("SET SERVER ROLE TO 'default'");
|
||||
assert!(qr.try_execute_command(query) != None);
|
||||
assert!(!qr.query_parser_enabled());
|
||||
assert!(qr.query_parser_enabled());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user