mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(mysql): honor connection timeout when listing databases
Closes #4244
This commit is contained in:
@@ -9,6 +9,7 @@ use sqlparser::dialect::MySqlDialect;
|
||||
use sqlparser::parser::Parser;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
@@ -2135,12 +2136,16 @@ const DATABASE_LIST_QUERY_PLAN: [(&str, bool); 2] =
|
||||
[(SHOW_DATABASES_SQL, true), (INFORMATION_SCHEMA_DATABASES_SQL, false)];
|
||||
|
||||
pub async fn list_databases(pool: &MySqlPool) -> Result<Vec<DatabaseInfo>, String> {
|
||||
list_databases_with_timeout(pool, super::connection_timeout()).await
|
||||
}
|
||||
|
||||
pub async fn list_databases_with_timeout(pool: &MySqlPool, timeout: Duration) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let [(primary_sql, primary_catalogless), (fallback_sql, fallback_catalogless)] = DATABASE_LIST_QUERY_PLAN;
|
||||
match list_databases_with_query(pool, primary_sql, primary_catalogless).await {
|
||||
match list_databases_with_query(pool, primary_sql, primary_catalogless, timeout).await {
|
||||
Ok(databases) => Ok(databases),
|
||||
Err(err) => {
|
||||
log::debug!("Falling back to information_schema.SCHEMATA after SHOW DATABASES failed: {err}");
|
||||
list_databases_with_query(pool, fallback_sql, fallback_catalogless).await
|
||||
list_databases_with_query(pool, fallback_sql, fallback_catalogless, timeout).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2183,15 +2188,23 @@ async fn list_databases_with_query(
|
||||
pool: &MySqlPool,
|
||||
sql: &str,
|
||||
include_catalogless_when_blank: bool,
|
||||
timeout: Duration,
|
||||
) -> Result<Vec<DatabaseInfo>, String> {
|
||||
let mut conn = get_conn_with_timeout(pool, super::connection_timeout()).await?;
|
||||
let mut conn = get_conn_with_timeout(pool, timeout).await?;
|
||||
let result = conn.query_iter(sql).await.map_err(|e| e.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|e| e.to_string())?;
|
||||
Ok(database_infos_from_names(rows.iter().map(|row| get_str(row, 0)), include_catalogless_when_blank))
|
||||
}
|
||||
|
||||
pub async fn list_databases_show(pool: &MySqlPool) -> Result<Vec<DatabaseInfo>, String> {
|
||||
list_databases_with_query(pool, SHOW_DATABASES_SQL, true).await
|
||||
list_databases_show_with_timeout(pool, super::connection_timeout()).await
|
||||
}
|
||||
|
||||
pub async fn list_databases_show_with_timeout(
|
||||
pool: &MySqlPool,
|
||||
timeout: Duration,
|
||||
) -> Result<Vec<DatabaseInfo>, String> {
|
||||
list_databases_with_query(pool, SHOW_DATABASES_SQL, true, timeout).await
|
||||
}
|
||||
|
||||
pub(super) fn database_infos_from_names(
|
||||
@@ -3876,12 +3889,8 @@ async fn get_conn_with_timeout_and_cancel(
|
||||
timeout: Duration,
|
||||
cancel_token: Option<&CancellationToken>,
|
||||
) -> Result<mysql_async::Conn, String> {
|
||||
let get_future = async {
|
||||
tokio::time::timeout(timeout, pool.get_conn())
|
||||
.await
|
||||
.map_err(|_| "MySQL get connection timed out".to_string())?
|
||||
.map_err(|e| e.to_string())
|
||||
};
|
||||
let get_future =
|
||||
connection_result_with_timeout(timeout, async { pool.get_conn().await.map_err(|error| error.to_string()) });
|
||||
|
||||
match cancel_token {
|
||||
Some(token) => {
|
||||
@@ -3896,10 +3905,14 @@ async fn get_conn_with_timeout_and_cancel(
|
||||
}
|
||||
|
||||
pub async fn get_conn_with_timeout(pool: &MySqlPool, timeout: Duration) -> Result<mysql_async::Conn, String> {
|
||||
tokio::time::timeout(timeout, pool.get_conn())
|
||||
.await
|
||||
.map_err(|_| "MySQL get connection timed out".to_string())?
|
||||
.map_err(|e| e.to_string())
|
||||
connection_result_with_timeout(timeout, async { pool.get_conn().await.map_err(|error| error.to_string()) }).await
|
||||
}
|
||||
|
||||
async fn connection_result_with_timeout<T, F>(timeout: Duration, future: F) -> Result<T, String>
|
||||
where
|
||||
F: Future<Output = Result<T, String>>,
|
||||
{
|
||||
tokio::time::timeout(timeout, future).await.map_err(|_| "MySQL get connection timed out".to_string())?
|
||||
}
|
||||
|
||||
async fn ping_conn_with_timeout_and_cancel(
|
||||
@@ -5302,6 +5315,38 @@ mod tests {
|
||||
assert_eq!(first_column_value::<String>(&row), Some(String::new()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mysql_database_list_timeout_controls_checkout_deadline() {
|
||||
let exact = connection_result_with_timeout(Duration::from_millis(100), async {
|
||||
tokio::time::sleep(Duration::from_millis(60)).await;
|
||||
Ok::<_, String>("connection")
|
||||
});
|
||||
let adjacent = connection_result_with_timeout(Duration::from_millis(50), async {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
Ok::<_, String>("connection")
|
||||
});
|
||||
let configured_five = connection_result_with_timeout(Duration::from_millis(50), async {
|
||||
tokio::time::sleep(Duration::from_millis(60)).await;
|
||||
Ok::<_, String>("connection")
|
||||
});
|
||||
|
||||
let (exact, adjacent, configured_five) = tokio::join!(exact, adjacent, configured_five);
|
||||
|
||||
assert_eq!(exact, Ok("connection"));
|
||||
assert_eq!(adjacent, Ok("connection"));
|
||||
assert_eq!(configured_five, Err("MySQL get connection timed out".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mysql_database_list_timeout_preserves_immediate_errors_and_fallback_plan() {
|
||||
let error =
|
||||
connection_result_with_timeout(Duration::from_secs(10), async { Err::<(), _>("driver error".to_string()) })
|
||||
.await;
|
||||
|
||||
assert_eq!(error, Err("driver error".to_string()));
|
||||
assert_eq!(DATABASE_LIST_QUERY_PLAN, [(SHOW_DATABASES_SQL, true), (INFORMATION_SCHEMA_DATABASES_SQL, false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_first_column_value_handles_null_and_conversion_failures() {
|
||||
let null_row = mysql_test_row(vec![Value::NULL, Value::Bytes(b"ignored".to_vec())]);
|
||||
|
||||
@@ -170,6 +170,12 @@ fn agent_metadata_timeout(config: Option<&ConnectionConfig>) -> Option<Duration>
|
||||
}
|
||||
}
|
||||
|
||||
fn mysql_database_list_timeout(config: Option<&ConnectionConfig>) -> Duration {
|
||||
config
|
||||
.map(|config| Duration::from_secs(config.effective_connect_timeout_secs()))
|
||||
.unwrap_or_else(db::connection_timeout)
|
||||
}
|
||||
|
||||
pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Result<Vec<db::DatabaseInfo>, String> {
|
||||
retry_metadata_connection(state, connection_id, None, || list_databases_once(state, connection_id)).await
|
||||
}
|
||||
@@ -611,6 +617,7 @@ async fn list_databases_once(state: &AppState, connection_id: &str) -> Result<Ve
|
||||
}
|
||||
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
let mysql_database_list_timeout = mysql_database_list_timeout(db_config.as_ref());
|
||||
let connections = state.connections.read().await;
|
||||
let pool = connections.get(connection_id).ok_or("Connection not found")?;
|
||||
|
||||
@@ -621,11 +628,12 @@ async fn list_databases_once(state: &AppState, connection_id: &str) -> Result<Ve
|
||||
db::dolt::list_databases(p).await
|
||||
}
|
||||
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(db::mysql_compatible::uses_show_metadata) => {
|
||||
db::mysql::list_databases_show(p)
|
||||
db::mysql::list_databases_show_with_timeout(p, mysql_database_list_timeout)
|
||||
.await
|
||||
.map(|databases| filter_mysql_system_databases_for_config(databases, db_config.as_ref()))
|
||||
}
|
||||
PoolKind::Mysql(p, mode) => dispatch_mysql!(p, mode, db::mysql::list_databases, db::ob_oracle::list_databases),
|
||||
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => db::ob_oracle::list_databases(p).await,
|
||||
PoolKind::Mysql(p, _) => db::mysql::list_databases_with_timeout(p, mysql_database_list_timeout).await,
|
||||
PoolKind::Postgres(p) => db::postgres::list_databases(p).await,
|
||||
PoolKind::Sqlite(p) => db::sqlite::list_databases(p).await,
|
||||
PoolKind::Rqlite(client) => db::rqlite_driver::list_databases(client).await,
|
||||
@@ -2671,13 +2679,14 @@ mod tests {
|
||||
filter_mysql_system_databases_for_config, filter_object_infos, filter_table_infos, filter_visible_schema_names,
|
||||
gaussdb_m_view_object_source_sql, gbase8a_object_statistics_sql, is_agent_postgres_metadata_fallback_config,
|
||||
is_mysql_external_driver_config, is_retryable_metadata_error, metadata_error_action,
|
||||
metadata_name_or_comment_matches, mysql_external_driver_ddl_from_query_result, mysql_external_driver_ddl_sql,
|
||||
mysql_object_source_ddl_column_index, mysql_object_source_sql, mysql_table_list_source_for_config,
|
||||
mysql_table_metadata_catalog, normalize_information_schema_table_type, oracle_columns_from_query_result,
|
||||
oracle_columns_sql, oracle_object_statistics_dba_segments_sql, oracle_object_statistics_from_query_result,
|
||||
oracle_object_statistics_rows_only_sql, oracle_object_statistics_sql,
|
||||
oracle_object_statistics_user_segments_sql, oracle_table_comment_from_query_result, oracle_table_comment_sql,
|
||||
oracle_table_comments_sql, presto_like_columns_from_query_result, presto_like_information_schema_columns_sql,
|
||||
metadata_name_or_comment_matches, mysql_database_list_timeout, mysql_external_driver_ddl_from_query_result,
|
||||
mysql_external_driver_ddl_sql, mysql_object_source_ddl_column_index, mysql_object_source_sql,
|
||||
mysql_table_list_source_for_config, mysql_table_metadata_catalog, normalize_information_schema_table_type,
|
||||
oracle_columns_from_query_result, oracle_columns_sql, oracle_object_statistics_dba_segments_sql,
|
||||
oracle_object_statistics_from_query_result, oracle_object_statistics_rows_only_sql,
|
||||
oracle_object_statistics_sql, oracle_object_statistics_user_segments_sql,
|
||||
oracle_table_comment_from_query_result, oracle_table_comment_sql, oracle_table_comments_sql,
|
||||
presto_like_columns_from_query_result, presto_like_information_schema_columns_sql,
|
||||
presto_like_information_schema_tables_sql, presto_like_tables_from_query_result, replace_metadata_runtime,
|
||||
should_query_oracle_columns_via_sql_first, table_comments_from_query_result, table_name_filter_matches,
|
||||
tdengine_table_comment_like_pattern, tdengine_table_comment_sql, tdengine_table_comments_sql,
|
||||
@@ -2844,6 +2853,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_database_list_timeout_uses_configured_and_effective_bounds() {
|
||||
let mut config = test_connection_config(DatabaseType::Mysql);
|
||||
|
||||
config.connect_timeout_secs = 10;
|
||||
assert_eq!(mysql_database_list_timeout(Some(&config)), Duration::from_secs(10));
|
||||
|
||||
config.connect_timeout_secs = 0;
|
||||
assert_eq!(
|
||||
mysql_database_list_timeout(Some(&config)),
|
||||
Duration::from_secs(crate::models::connection::default_connect_timeout_secs())
|
||||
);
|
||||
|
||||
config.connect_timeout_secs = 500;
|
||||
assert_eq!(mysql_database_list_timeout(Some(&config)), Duration::from_secs(300));
|
||||
assert_eq!(mysql_database_list_timeout(None), db::connection_timeout());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turso_schema_dispatch_lists_databases_and_tables() {
|
||||
let (base_url, server) = spawn_turso_table_server().await;
|
||||
|
||||
Reference in New Issue
Block a user