From 39505896a6a9228610cab330be504a191f1f0be5 Mon Sep 17 00:00:00 2001 From: lxk955 <164191365+lxk955@users.noreply.github.com> Date: Sun, 11 Oct 2026 08:45:37 +0800 Subject: [PATCH] fix(mysql): resolve ProxySQL hostgroup lock via literal floor setup --- crates/dbx-driver-mysql/src/mysql.rs | 114 ++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/crates/dbx-driver-mysql/src/mysql.rs b/crates/dbx-driver-mysql/src/mysql.rs index cf29cd375b..f50935daf6 100644 --- a/crates/dbx-driver-mysql/src/mysql.rs +++ b/crates/dbx-driver-mysql/src/mysql.rs @@ -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 { 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 { - 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 { - 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 { @@ -2452,6 +2469,30 @@ fn mysql_local_infile_paths(url: &str) -> Vec { .collect() } +fn mysql_url_setup_mode(url: &str) -> Option { + 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 { let chars: Vec = 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