fix: 修复应用长时间挂后台后恢复时表格数据永久加载的问题
三层防护: - MySQL 连接获取时执行 ping 健康检查,死连接自动重连 - macOS Reopen 事件 + 前端 visibilitychange 触发连接刷新 - 前端 Promise.race 超时保护,确保 loading 状态不会超过 60s
This commit is contained in:
1 parent
6c0db6f517
commit
b54eda1889
8 files changed
+157
-13
No files matched your search
@@ -541,6 +541,86 @@ impl AppState {
|
||||
self.proxy_tunnels.stop_tunnel(connection_id).await;
|
||||
}
|
||||
|
||||
pub async fn refresh_connections(&self) {
|
||||
let mut dead_keys = Vec::new();
|
||||
|
||||
// Check health of all connection pools
|
||||
{
|
||||
let conns = self.connections.read().await;
|
||||
for (key, pool) in conns.iter() {
|
||||
let healthy = match pool {
|
||||
PoolKind::Mysql(p, _) => {
|
||||
match db::mysql::get_conn_with_health_check(p).await {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::warn!("MySQL connection pool '{key}' is unhealthy: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
PoolKind::Postgres(p) => {
|
||||
match p.get().await {
|
||||
Ok(client) => {
|
||||
match client.simple_query("SELECT 1").await {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
log::warn!("PostgreSQL connection pool '{key}' is unhealthy: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("PostgreSQL connection pool '{key}' is unhealthy: {e}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => true, // Skip non-SQL pools
|
||||
};
|
||||
if !healthy {
|
||||
dead_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove dead pools
|
||||
if !dead_keys.is_empty() {
|
||||
let mut conns = self.connections.write().await;
|
||||
for key in &dead_keys {
|
||||
if let Some(pool) = conns.remove(key) {
|
||||
close_pool_kind(pool).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-establish SSH tunnels that have died
|
||||
let tunnel_connection_ids: Vec<String> = {
|
||||
let configs = self.configs.read().await;
|
||||
configs
|
||||
.iter()
|
||||
.filter(|(_, c)| c.ssh_enabled && !c.ssh_host.is_empty())
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect()
|
||||
};
|
||||
for connection_id in tunnel_connection_ids {
|
||||
self.tunnels.stop_tunnel(&connection_id).await;
|
||||
// Tunnels will be re-created on next pool access via connection_host_port
|
||||
}
|
||||
|
||||
// Re-establish proxy tunnels
|
||||
let proxy_connection_ids: Vec<String> = {
|
||||
let configs = self.configs.read().await;
|
||||
configs
|
||||
.iter()
|
||||
.filter(|(_, c)| c.proxy_enabled && !c.proxy_host.is_empty())
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect()
|
||||
};
|
||||
for connection_id in proxy_connection_ids {
|
||||
self.proxy_tunnels.stop_tunnel(&connection_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_connection_pools(&self, connection_id: &str) {
|
||||
let mut conns = self.connections.write().await;
|
||||
let keys_to_remove: Vec<String> = conns
|
||||
|
||||
@@ -798,13 +798,26 @@ fn query_result_row_limit(max_rows: Option<usize>) -> usize {
|
||||
max_rows.unwrap_or(crate::query::MAX_ROWS).max(1)
|
||||
}
|
||||
|
||||
/// Get a connection from the pool with a health check. If the connection is dead
|
||||
/// (e.g. after app was backgrounded), it tries again with a fresh connection.
|
||||
pub async fn get_conn_with_health_check(pool: &MySqlPool) -> Result<mysql_async::Conn, String> {
|
||||
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
|
||||
match conn.ping().await {
|
||||
Ok(()) => Ok(conn),
|
||||
Err(_) => {
|
||||
let _ = conn.disconnect().await;
|
||||
pool.get_conn().await.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_result_set_with_text_protocol(
|
||||
pool: &MySqlPool,
|
||||
sql: &str,
|
||||
row_limit: usize,
|
||||
start: Instant,
|
||||
) -> Result<QueryResult, String> {
|
||||
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
|
||||
let mut conn = get_conn_with_health_check(pool).await?;
|
||||
let mut result = conn.query_iter(sql).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect();
|
||||
|
||||
@@ -846,7 +859,7 @@ async fn execute_result_set_with_prepared_protocol(
|
||||
row_limit: usize,
|
||||
start: Instant,
|
||||
) -> Result<QueryResult, String> {
|
||||
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
|
||||
let mut conn = get_conn_with_health_check(pool).await?;
|
||||
let mut result = conn.exec_iter(sql, ()).await.map_err(|e| e.to_string())?;
|
||||
let columns: Vec<String> = result.columns_ref().iter().map(|c| c.name_str().to_string()).collect();
|
||||
|
||||
@@ -908,7 +921,7 @@ pub async fn execute_query_with_max_rows(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut conn = pool.get_conn().await.map_err(|e| e.to_string())?;
|
||||
let mut conn = get_conn_with_health_check(pool).await?;
|
||||
let previous_explicit_timestamp_defaults = enable_explicit_timestamp_defaults_for_query(&mut conn, sql).await;
|
||||
let result = match conn.query_iter(sql).await {
|
||||
Ok(result) => result,
|
||||
|
||||
@@ -355,7 +355,7 @@ pub async fn connect(url: &str, fallback_timeout: Duration) -> Result<Pool, Stri
|
||||
let pg_config = tokio_postgres::Config::from_str(&postgres_url.url)
|
||||
.map_err(|e| format!("Invalid PostgreSQL connection URL: {e}"))?;
|
||||
|
||||
let mgr_config = ManagerConfig { recycling_method: RecyclingMethod::Fast };
|
||||
let mgr_config = ManagerConfig { recycling_method: RecyclingMethod::Verified };
|
||||
let tls_config = postgres_tls_config(
|
||||
&pg_config,
|
||||
&postgres_url.ssl_files,
|
||||
|
||||
@@ -1071,7 +1071,7 @@ async fn exec_tx_mysql_inner(
|
||||
statements: &[String],
|
||||
start: std::time::Instant,
|
||||
) -> Result<db::QueryResult, String> {
|
||||
let mut conn = pool.get_conn().await.map_err(|e| format!("Failed to acquire connection: {}", e))?;
|
||||
let mut conn = db::mysql::get_conn_with_health_check(&pool).await?;
|
||||
conn.query_drop("START TRANSACTION").await.map_err(|e| format!("Failed to begin transaction: {}", e))?;
|
||||
let mut total_affected: u64 = 0;
|
||||
for (i, sql) in statements.iter().enumerate() {
|
||||
|
||||
Reference in new issue
Block a user