fix: 修复应用长时间挂后台后恢复时表格数据永久加载的问题

三层防护:
- MySQL 连接获取时执行 ping 健康检查,死连接自动重连
- macOS Reopen 事件 + 前端 visibilitychange 触发连接刷新
- 前端 Promise.race 超时保护,确保 loading 状态不会超过 60s
This commit is contained in:
t8y2 committed 2026-05-29 15:12:14 +08:00
1 parent 6c0db6f517
commit b54eda1889
8 files changed
+157 -13

No files matched your search

+80
View File
@@ -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
+16 -3
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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() {