修复安卓端PostgreSQL连接长时间转圈无响应

This commit is contained in:
JSR
2026-03-19 21:35:27 +08:00
parent a782da9037
commit 10eb07573c
5 changed files with 191 additions and 49 deletions
+34 -12
View File
@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use std::sync::Arc; use std::sync::Arc;
use tauri::{AppHandle, Emitter, Manager, State}; use tauri::{AppHandle, Emitter, Manager, State};
use tokio::time::{timeout, Duration};
use crate::db::connection::{create_postgres_connection, create_sqlite_connection}; use crate::db::connection::{create_postgres_connection, create_sqlite_connection};
use crate::db::connection::DatabaseType; use crate::db::connection::DatabaseType;
@@ -15,6 +16,9 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
use super::response::IpcResponse; use super::response::IpcResponse;
const DB_CONNECT_TIMEOUT_SECS: u64 = 15;
const DB_MIGRATION_TIMEOUT_SECS: u64 = 20;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestConnectionResult { pub struct TestConnectionResult {
pub success: bool, pub success: bool,
@@ -775,18 +779,28 @@ pub async fn db_test_connection(
} else if connection_string.starts_with("postgres://") } else if connection_string.starts_with("postgres://")
|| connection_string.starts_with("postgresql://") || connection_string.starts_with("postgresql://")
{ {
match create_postgres_connection(&connection_string).await { let connection_result = timeout(
Ok(conn) => { Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
create_postgres_connection(&connection_string),
)
.await;
match connection_result {
Err(_) => TestConnectionResult {
success: false,
error: Some("PostgreSQL 连接超时,请检查网络或连接字符串".to_string()),
},
Ok(Err(e)) => TestConnectionResult {
success: false,
error: Some(e.to_string()),
},
Ok(Ok(conn)) => {
let _ = conn.close().await; let _ = conn.close().await;
TestConnectionResult { TestConnectionResult {
success: true, success: true,
error: None, error: None,
} }
} }
Err(e) => TestConnectionResult {
success: false,
error: Some(e.to_string()),
},
} }
} else { } else {
let path = connection_string.as_str(); let path = connection_string.as_str();
@@ -819,12 +833,20 @@ pub async fn db_switch_connection(
let (db_type, saved_connection_string, saved_status, conn) = if connection_string.starts_with("postgres://") let (db_type, saved_connection_string, saved_status, conn) = if connection_string.starts_with("postgres://")
|| connection_string.starts_with("postgresql://") || connection_string.starts_with("postgresql://")
{ {
let conn = create_postgres_connection(&connection_string) let conn = timeout(
.await Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
.map_err(|e| e.to_string())?; create_postgres_connection(&connection_string),
run_migration(&conn, DatabaseType::PostgreSQL) )
.await .await
.map_err(|e| e.to_string())?; .map_err(|_| "PostgreSQL 连接超时,请检查网络或连接字符串".to_string())?
.map_err(|e| e.to_string())?;
timeout(
Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS),
run_migration(&conn, DatabaseType::PostgreSQL),
)
.await
.map_err(|_| "PostgreSQL 初始化超时,请稍后重试".to_string())?
.map_err(|e| e.to_string())?;
( (
"postgresql".to_string(), "postgresql".to_string(),
+20 -12
View File
@@ -1,9 +1,26 @@
use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr}; use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr};
use std::time::Duration;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::{info, warn}; use tracing::{info, warn};
const DB_MAX_CONNECTIONS: u32 = 20;
const DB_MIN_CONNECTIONS: u32 = 1;
const DB_CONNECT_TIMEOUT_SECS: u64 = 12;
const DB_ACQUIRE_TIMEOUT_SECS: u64 = 12;
const DB_IDLE_TIMEOUT_SECS: u64 = 30;
fn apply_default_connect_options(opt: &mut ConnectOptions) {
opt.max_connections(DB_MAX_CONNECTIONS)
.min_connections(DB_MIN_CONNECTIONS)
.connect_timeout(Duration::from_secs(DB_CONNECT_TIMEOUT_SECS))
.acquire_timeout(Duration::from_secs(DB_ACQUIRE_TIMEOUT_SECS))
.idle_timeout(Duration::from_secs(DB_IDLE_TIMEOUT_SECS))
.sqlx_logging(true)
.sqlx_logging_level(tracing::log::LevelFilter::Info);
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum DatabaseType { pub enum DatabaseType {
SQLite, SQLite,
@@ -106,10 +123,7 @@ impl ConnectionManager {
); );
let mut opt = ConnectOptions::new(&url); let mut opt = ConnectOptions::new(&url);
opt.max_connections(100) apply_default_connect_options(&mut opt);
.min_connections(5)
.sqlx_logging(true)
.sqlx_logging_level(tracing::log::LevelFilter::Info);
let conn = Database::connect(opt).await?; let conn = Database::connect(opt).await?;
@@ -195,20 +209,14 @@ impl ConnectionManager {
pub async fn create_sqlite_connection(path: &str) -> Result<DatabaseConnection, DbErr> { pub async fn create_sqlite_connection(path: &str) -> Result<DatabaseConnection, DbErr> {
let url = format!("sqlite://{}?mode=rwc", path); let url = format!("sqlite://{}?mode=rwc", path);
let mut opt = ConnectOptions::new(&url); let mut opt = ConnectOptions::new(&url);
opt.max_connections(100) apply_default_connect_options(&mut opt);
.min_connections(5)
.sqlx_logging(true)
.sqlx_logging_level(tracing::log::LevelFilter::Info);
Database::connect(opt).await Database::connect(opt).await
} }
pub async fn create_postgres_connection(url: &str) -> Result<DatabaseConnection, DbErr> { pub async fn create_postgres_connection(url: &str) -> Result<DatabaseConnection, DbErr> {
let mut opt = ConnectOptions::new(url); let mut opt = ConnectOptions::new(url);
opt.max_connections(100) apply_default_connect_options(&mut opt);
.min_connections(5)
.sqlx_logging(true)
.sqlx_logging_level(tracing::log::LevelFilter::Info);
Database::connect(opt).await Database::connect(opt).await
} }
+65 -17
View File
@@ -13,6 +13,7 @@ use crate::db::migration::run_migration;
use crate::services::settings::{SettingsKey, SettingsValue}; use crate::services::settings::{SettingsKey, SettingsValue};
use crate::{commands::*, state::AppState}; use crate::{commands::*, state::AppState};
use tauri::{App, Manager}; use tauri::{App, Manager};
use tokio::time::{timeout, Duration};
#[cfg(desktop)] #[cfg(desktop)]
use tauri::{ use tauri::{
image::Image, image::Image,
@@ -125,6 +126,9 @@ pub fn setup_app(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
} }
fn setup_database(app: &mut App) -> Result<(), Box<dyn std::error::Error>> { fn setup_database(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
const DB_CONNECT_TIMEOUT_SECS: u64 = 15;
const DB_MIGRATION_TIMEOUT_SECS: u64 = 20;
let handle = app.handle().clone(); let handle = app.handle().clone();
let db_path = if cfg!(all(debug_assertions, desktop)) { let db_path = if cfg!(all(debug_assertions, desktop)) {
std::path::PathBuf::from("data.sql") std::path::PathBuf::from("data.sql")
@@ -166,9 +170,67 @@ fn setup_database(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
}; };
if !pg_connection_string.trim().is_empty() { if !pg_connection_string.trim().is_empty() {
match create_postgres_connection(&pg_connection_string).await { match timeout(
Ok(pg_conn) => { Duration::from_secs(DB_CONNECT_TIMEOUT_SECS),
if let Err(e) = run_migration(&pg_conn, DatabaseType::PostgreSQL).await { create_postgres_connection(&pg_connection_string),
)
.await
{
Err(_) => {
let timeout_message = "PostgreSQL auto-connect timeout".to_string();
eprintln!(
"PostgreSQL auto-connect timed out on startup, fallback to sqlite"
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": timeout_message
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
Ok(Err(e)) => {
eprintln!("PostgreSQL auto-connect failed, fallback to sqlite: {}", e);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": e.to_string()
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
Ok(Ok(pg_conn)) => {
let migration_result = timeout(
Duration::from_secs(DB_MIGRATION_TIMEOUT_SECS),
run_migration(&pg_conn, DatabaseType::PostgreSQL),
)
.await;
if let Err(_) = migration_result {
let timeout_message = "PostgreSQL migration timeout".to_string();
eprintln!(
"PostgreSQL migration timed out on startup, fallback to sqlite"
);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": timeout_message
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
} else if let Ok(Err(e)) = migration_result {
eprintln!("PostgreSQL migration failed on startup, fallback to sqlite: {}", e); eprintln!("PostgreSQL migration failed on startup, fallback to sqlite: {}", e);
settings settings
.set_value( .set_value(
@@ -196,20 +258,6 @@ fn setup_database(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Auto connected to PostgreSQL from saved connection string"); eprintln!("Auto connected to PostgreSQL from saved connection string");
} }
} }
Err(e) => {
eprintln!("PostgreSQL auto-connect failed, fallback to sqlite: {}", e);
settings
.set_value(
SettingsKey::PgConnectionStatus,
SettingsValue::Json(serde_json::json!({
"connected": false,
"type": "sqlite",
"error": e.to_string()
})),
)
.await
.map_err(|err| format!("Failed to save pg status: {}", err))?;
}
} }
} else { } else {
settings settings
+29 -3
View File
@@ -34,6 +34,20 @@ interface reasonItem {
const deepClone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T const deepClone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T
const withTimeout = async (promise: Promise<any>, ms: number, timeoutMessage: string): Promise<any> => {
let timer: ReturnType<typeof setTimeout> | undefined
try {
return await Promise.race<any>([
promise,
new Promise<any>((_, reject) => {
timer = setTimeout(() => reject(new Error(timeoutMessage)), ms)
}),
])
} finally {
if (timer) clearTimeout(timer)
}
}
const presetPrimaryColors = [ const presetPrimaryColors = [
"#1677FF", "#1677FF",
"#2F54EB", "#2F54EB",
@@ -363,18 +377,30 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
setPgAutoLoading(true) setPgAutoLoading(true)
try { try {
if (!(window as any).api) throw new Error("api not ready") if (!(window as any).api) throw new Error("api not ready")
const switchRes = await (window as any).api.dbSwitchConnection(connectionString) const switchRes = await withTimeout(
(window as any).api.dbSwitchConnection(connectionString),
25_000,
"连接 PostgreSQL 超时,请检查网络或连接字符串"
)
if (!switchRes?.success || switchRes?.data?.type !== "postgresql") { if (!switchRes?.success || switchRes?.data?.type !== "postgresql") {
throw new Error(switchRes?.message || t("settings.database.switchFailed")) throw new Error(switchRes?.message || t("settings.database.switchFailed"))
} }
const previewRes = await (window as any).api.dbSyncPreview() const previewRes = await withTimeout(
(window as any).api.dbSyncPreview(),
25_000,
"同步预检查超时,请稍后重试"
)
if (!previewRes?.success || !previewRes?.data?.can_sync) { if (!previewRes?.success || !previewRes?.data?.can_sync) {
throw new Error(previewRes?.message || previewRes?.data?.message || t("settings.database.uploadFailed")) throw new Error(previewRes?.message || previewRes?.data?.message || t("settings.database.uploadFailed"))
} }
if (previewRes?.data?.need_sync) { if (previewRes?.data?.need_sync) {
const syncApplyRes = await (window as any).api.dbSyncApply("keep_local") const syncApplyRes = await withTimeout(
(window as any).api.dbSyncApply("keep_local"),
45_000,
"执行同步超时,请稍后重试"
)
if (!syncApplyRes?.success || !syncApplyRes?.data?.success) { if (!syncApplyRes?.success || !syncApplyRes?.data?.success) {
throw new Error( throw new Error(
syncApplyRes?.data?.message || syncApplyRes?.message || t("settings.database.uploadFailed") syncApplyRes?.data?.message || syncApplyRes?.message || t("settings.database.uploadFailed")
+43 -5
View File
@@ -27,6 +27,20 @@ type appSettings = {
auto_score_enabled?: boolean auto_score_enabled?: boolean
} }
const withTimeout = async (promise: Promise<any>, ms: number, timeoutMessage: string): Promise<any> => {
let timer: ReturnType<typeof setTimeout> | undefined
try {
return await Promise.race<any>([
promise,
new Promise<any>((_, reject) => {
timer = setTimeout(() => reject(new Error(timeoutMessage)), ms)
}),
])
} finally {
if (timer) clearTimeout(timer)
}
}
export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => { export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [activeTab, setActiveTab] = useState("appearance") const [activeTab, setActiveTab] = useState("appearance")
@@ -307,7 +321,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
} }
setPgTestLoading(true) setPgTestLoading(true)
try { try {
const res = await (window as any).api.dbTestConnection(pgConnectionString) const res = await withTimeout(
(window as any).api.dbTestConnection(pgConnectionString),
20_000,
"测试连接超时,请检查网络或连接字符串"
)
if (res.success && res.data?.success) { if (res.success && res.data?.success) {
messageApi.success(t("settings.database.connectionTestSuccess")) messageApi.success(t("settings.database.connectionTestSuccess"))
} else { } else {
@@ -324,9 +342,17 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
const switchToPg = async () => { const switchToPg = async () => {
if (!(window as any).api) return if (!(window as any).api) return
if (!pgConnectionString.trim()) {
messageApi.warning(t("settings.database.enterConnectionString"))
return
}
setPgSwitchLoading(true) setPgSwitchLoading(true)
try { try {
const res = await (window as any).api.dbSwitchConnection(pgConnectionString) const res = await withTimeout(
(window as any).api.dbSwitchConnection(pgConnectionString.trim()),
25_000,
"切换 PostgreSQL 超时,请检查网络或连接字符串"
)
if (res.success) { if (res.success) {
messageApi.success( messageApi.success(
t("settings.database.switchedTo", { t("settings.database.switchedTo", {
@@ -348,7 +374,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
if (!(window as any).api) return if (!(window as any).api) return
setPgSwitchLoading(true) setPgSwitchLoading(true)
try { try {
const res = await (window as any).api.dbSwitchConnection("") const res = await withTimeout(
(window as any).api.dbSwitchConnection(""),
15_000,
"切换 SQLite 超时,请稍后重试"
)
if (res.success) { if (res.success) {
messageApi.success(t("settings.database.switchedToSQLite")) messageApi.success(t("settings.database.switchedToSQLite"))
setPgConnectionString("") setPgConnectionString("")
@@ -372,7 +402,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
return return
} }
const previewRes = await (window as any).api.dbSyncPreview() const previewRes = await withTimeout(
(window as any).api.dbSyncPreview(),
25_000,
"同步预检查超时,请稍后重试"
)
if (!previewRes.success || !previewRes.data?.can_sync) { if (!previewRes.success || !previewRes.data?.can_sync) {
messageApi.error(previewRes.message || previewRes.data?.message || t("settings.database.uploadFailed")) messageApi.error(previewRes.message || previewRes.data?.message || t("settings.database.uploadFailed"))
return return
@@ -394,7 +428,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
onOk: async () => { onOk: async () => {
setPgUploadLoading(true) setPgUploadLoading(true)
try { try {
const applyRes = await (window as any).api.dbSyncApply("keep_local") const applyRes = await withTimeout(
(window as any).api.dbSyncApply("keep_local"),
45_000,
"执行同步超时,请稍后重试"
)
if (applyRes.success && applyRes.data?.success) { if (applyRes.success && applyRes.data?.success) {
messageApi.success( messageApi.success(
applyRes.data?.message || t("settings.database.uploadSuccess") applyRes.data?.message || t("settings.database.uploadSuccess")