mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(mysql): resolve ProxySQL hostgroup lock via literal floor setup
This commit is contained in:
@@ -1396,7 +1396,7 @@ pub async fn connect_with_ca_cert_pool_limit_idle_and_setup_database(
|
||||
idle_timeout_secs,
|
||||
setup_database,
|
||||
extra_setup_queries,
|
||||
MySqlSetupMode::Standard,
|
||||
MySqlSetupMode::default_for_url(url),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1648,6 +1648,10 @@ const MYSQL_DEFAULT_CHARSET: &str = "utf8mb4";
|
||||
const MYSQL_LEGACY_CHARSET: &str = "utf8";
|
||||
|
||||
impl MySqlSetupMode {
|
||||
pub fn default_for_url(url: &str) -> Self {
|
||||
mysql_url_setup_mode(url).unwrap_or(Self::Standard)
|
||||
}
|
||||
|
||||
fn group_concat_max_len_query(self, url: &str) -> Option<String> {
|
||||
match self {
|
||||
// An explicit Connector/J style `sessionVariables=group_concat_max_len=...`
|
||||
@@ -1982,6 +1986,13 @@ fn mysql_group_concat_setup_fallback_mode(setup_mode: MySqlSetupMode, error: &st
|
||||
// `Unknown system variable 'group_concat_'` with `max_len` cut off
|
||||
// (issue #10197). The truncated prefix is specific enough on its own.
|
||||
let proxy_truncated_variable_rejected = lower.contains("unknown system variable 'group_concat_'");
|
||||
// ProxySQL rejects queries with error 9006 when an unparseable expression SET
|
||||
// (such as `cast(greatest(...))`) causes ProxySQL to lock the connection to
|
||||
// hostgroup 0 under `mysql-set_query_lock_on_hostgroup=1`, conflicting with
|
||||
// read/write split rules routing queries to another hostgroup (issue #11307).
|
||||
let proxysql_hostgroup_locked = lower.contains("error 9006")
|
||||
|| (lower.contains("proxysql") && lower.contains("locked to hostgroup"))
|
||||
|| (lower.contains("connection is locked to hostgroup") && lower.contains("trying to reach hostgroup"));
|
||||
if (floor_statement_rejected && (setup_query_rejected || setup_value_rejected || setup_argument_rejected))
|
||||
|| txsql_truncated_name_rejected
|
||||
|| gaea_setup_expression_rejected
|
||||
@@ -1989,6 +2000,7 @@ fn mysql_group_concat_setup_fallback_mode(setup_mode: MySqlSetupMode, error: &st
|
||||
|| sphinxql_setup_query_rejected
|
||||
|| gateway_session_variable_rejected
|
||||
|| proxy_truncated_variable_rejected
|
||||
|| proxysql_hostgroup_locked
|
||||
{
|
||||
return Some(MySqlSetupMode::Compatible);
|
||||
}
|
||||
@@ -2187,7 +2199,7 @@ fn mysql_ssl_opts(
|
||||
}
|
||||
|
||||
fn mysql_setup_queries(url: &str, extra_setup_queries: &[String]) -> Vec<String> {
|
||||
mysql_setup_queries_with_mode(url, extra_setup_queries, MySqlSetupMode::Standard)
|
||||
mysql_setup_queries_with_mode(url, extra_setup_queries, MySqlSetupMode::default_for_url(url))
|
||||
}
|
||||
|
||||
fn mysql_setup_queries_for_database(
|
||||
@@ -2195,7 +2207,12 @@ fn mysql_setup_queries_for_database(
|
||||
setup_database: Option<&str>,
|
||||
extra_setup_queries: &[String],
|
||||
) -> Vec<String> {
|
||||
mysql_setup_queries_for_database_with_mode(url, setup_database, extra_setup_queries, MySqlSetupMode::Standard)
|
||||
mysql_setup_queries_for_database_with_mode(
|
||||
url,
|
||||
setup_database,
|
||||
extra_setup_queries,
|
||||
MySqlSetupMode::default_for_url(url),
|
||||
)
|
||||
}
|
||||
|
||||
fn mysql_setup_queries_with_mode(url: &str, extra_setup_queries: &[String], setup_mode: MySqlSetupMode) -> Vec<String> {
|
||||
@@ -2452,6 +2469,30 @@ fn mysql_local_infile_paths(url: &str) -> Vec<PathBuf> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn mysql_url_setup_mode(url: &str) -> Option<MySqlSetupMode> {
|
||||
let (_, query) = url.split_once('?')?;
|
||||
let query = query.split('#').next().unwrap_or(query);
|
||||
for segment in query.split('&') {
|
||||
let (key, value) = match segment.split_once('=') {
|
||||
Some((k, v)) => (k.trim(), v.trim()),
|
||||
None => (segment.trim(), "true"),
|
||||
};
|
||||
if key.eq_ignore_ascii_case("proxysql") && mysql_url_param_value_is_true(value) {
|
||||
return Some(MySqlSetupMode::LiteralFloor);
|
||||
}
|
||||
if key.eq_ignore_ascii_case("setupmode") || key.eq_ignore_ascii_case("setup_mode") {
|
||||
if value.eq_ignore_ascii_case("literalfloor") || value.eq_ignore_ascii_case("literal_floor") {
|
||||
return Some(MySqlSetupMode::LiteralFloor);
|
||||
} else if value.eq_ignore_ascii_case("compatible") {
|
||||
return Some(MySqlSetupMode::Compatible);
|
||||
} else if value.eq_ignore_ascii_case("standard") {
|
||||
return Some(MySqlSetupMode::Standard);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn split_mysql_session_variables(value: &str) -> Vec<String> {
|
||||
let chars: Vec<char> = value.chars().collect();
|
||||
let mut assignments = Vec::new();
|
||||
@@ -2619,6 +2660,7 @@ async fn verify_pool_connection(pool: &MySqlPool, timeout: Duration) -> Result<(
|
||||
super::with_connection_timeout("MySQL", timeout, async {
|
||||
let mut conn = pool.get_conn().await.map_err(|e| format!("MySQL connection failed: {e}"))?;
|
||||
conn.ping().await.map_err(|e| format!("MySQL ping failed: {e}"))?;
|
||||
conn.query_drop("SELECT 1").await.map_err(|e| format!("MySQL connection verification query failed: {e}"))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
@@ -2873,6 +2915,9 @@ fn is_dbx_handled_mysql_url_param(key: &str) -> bool {
|
||||
| "forceconnectiontimezonetosession"
|
||||
| "sessionvariables"
|
||||
| "localinfilepath"
|
||||
| "proxysql"
|
||||
| "setupmode"
|
||||
| "setup_mode"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9816,6 +9861,10 @@ mod tests {
|
||||
// is the floor expression failing to parse rather than the variable being
|
||||
// missing, so the literal rung still applies (issue #10197).
|
||||
"MySQL connection failed: Server error: `ERROR 1193 (HY000): Unknown system variable 'group_concat_'`",
|
||||
// ProxySQL error 9006 when expression SET locks connection to hostgroup 0 (issue #11307).
|
||||
"MySQL connection verification query failed: Server error: `ERROR 9006 (Y0000): ProxySQL Error: connection is locked to hostgroup 0 but trying to reach hostgroup 1`",
|
||||
"Server error: `ERROR 9006 (Y0000): ProxySQL Error: connection is locked to hostgroup 0 but trying to reach hostgroup 1`",
|
||||
"Server error: `ProxySQL Error: connection is locked to hostgroup 0 but trying to reach hostgroup 1`",
|
||||
] {
|
||||
let fallback = mysql_group_concat_setup_fallback_mode(MySqlSetupMode::Standard, error)
|
||||
.or_else(|| mysql_setup_probe_fallback_mode(MySqlSetupMode::Standard, url, error))
|
||||
@@ -9828,6 +9877,65 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_proxysql_hostgroup_locked_error_retries_with_literal_floor() {
|
||||
let error = "MySQL connection verification query failed: Server error: `ERROR 9006 (Y0000): ProxySQL Error: connection is locked to hostgroup 0 but trying to reach hostgroup 1`";
|
||||
let fallback = mysql_group_concat_setup_fallback_mode(MySqlSetupMode::Standard, error)
|
||||
.expect("ProxySQL error 9006 must trigger setup fallback");
|
||||
assert_eq!(fallback, MySqlSetupMode::Compatible);
|
||||
assert_eq!(
|
||||
mysql_setup_fallback_ladder(fallback, error),
|
||||
vec![MySqlSetupMode::Compatible, MySqlSetupMode::LiteralFloor]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_setup_mode_url_detection() {
|
||||
assert_eq!(
|
||||
MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app?proxysql=true"),
|
||||
MySqlSetupMode::LiteralFloor
|
||||
);
|
||||
assert_eq!(
|
||||
MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app?proxysql=1"),
|
||||
MySqlSetupMode::LiteralFloor
|
||||
);
|
||||
assert_eq!(
|
||||
MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app?setupMode=literalFloor"),
|
||||
MySqlSetupMode::LiteralFloor
|
||||
);
|
||||
assert_eq!(
|
||||
MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app?setup_mode=literal_floor"),
|
||||
MySqlSetupMode::LiteralFloor
|
||||
);
|
||||
assert_eq!(
|
||||
MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app?setupMode=compatible"),
|
||||
MySqlSetupMode::Compatible
|
||||
);
|
||||
assert_eq!(
|
||||
MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app?setupMode=standard"),
|
||||
MySqlSetupMode::Standard
|
||||
);
|
||||
assert_eq!(MySqlSetupMode::default_for_url("mysql://root:pw@host:3306/app"), MySqlSetupMode::Standard);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_setup_queries_prefers_literal_floor_for_proxysql_url() {
|
||||
let queries = mysql_setup_queries("mysql://host:3306/db?proxysql=true", &[]);
|
||||
assert_eq!(queries, vec!["USE `db`", "SET NAMES utf8mb4", "SET SESSION group_concat_max_len = 1048576"]);
|
||||
|
||||
let queries_flag = mysql_setup_queries("mysql://host:3306/db?setupMode=literalFloor", &[]);
|
||||
assert_eq!(queries_flag, vec!["USE `db`", "SET NAMES utf8mb4", "SET SESSION group_concat_max_len = 1048576"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_url_strips_proxysql_and_setupmode_parameters() {
|
||||
let url = "mysql://root:pw@host:3306/app?proxysql=true&setupMode=literalFloor&require_ssl=true";
|
||||
let async_url = mysql_async_url(url);
|
||||
assert!(!async_url.contains("proxysql"), "{async_url}");
|
||||
assert!(!async_url.contains("setupMode"), "{async_url}");
|
||||
assert_eq!(async_url.as_ref(), "mysql://root:pw@host:3306/app?require_ssl=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_group_concat_variable_level_rejections_skip_the_literal_floor() {
|
||||
// These servers reject the variable itself, so the plain literal cannot help and
|
||||
|
||||
Reference in New Issue
Block a user