fix(mysql): resolve ProxySQL hostgroup lock via literal floor setup

This commit is contained in:
lxk955
2026-10-11 08:45:37 +08:00
committed by GitHub
parent 035fecd17b
commit 39505896a6
+111 -3
View File
@@ -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