11use common:: config:: DbConfig ;
22use common:: errors:: MegaError ;
33use sea_orm:: { ConnectOptions , Database , DatabaseConnection } ;
4- use std:: { path:: Path , time:: Duration } ;
4+ use std:: {
5+ net:: { SocketAddr , TcpStream } ,
6+ path:: Path ,
7+ time:: Duration ,
8+ } ;
59use tracing:: log;
10+ use url:: Url ;
611
712use crate :: migration:: apply_migrations;
813
914use crate :: utils:: id_generator;
1015
11- /// Create a database connection.
12- /// When postgres is set but not available, it will fall back to sqlite automatically.
16+ /// Create a database connection with failover logic.
17+ ///
18+ /// This function attempts to connect to a database based on the provided configuration:
19+ /// - If PostgreSQL is specified but unavailable, it automatically falls back to SQLite
20+ /// - For local PostgreSQL connections, it first checks port reachability to avoid long timeouts
21+ ///
22+ /// The failover logic works as follows:
23+ /// 1. For PostgreSQL connections:
24+ /// - If the host is local (localhost, 127.0.0.1, etc.), performs a quick port check (100ms timeout)
25+ /// - If port is unreachable, immediately falls back to SQLite without waiting for a full connection timeout
26+ /// - If port is reachable but connection fails, logs the error and falls back to SQLite
27+ /// 2. For non-local PostgreSQL, attempts connection with normal timeouts (3 seconds)
28+ /// - On failure, logs the error and falls back to SQLite
29+ /// 3. For SQLite connections, connects directly without fallback
30+ ///
31+ /// After successful connection, applies any pending database migrations.
32+ ///
33+ /// This optimization helps avoid long waits when local PostgreSQL isn't running.
1334pub async fn database_connection ( db_config : & DbConfig ) -> DatabaseConnection {
1435 id_generator:: set_up_options ( ) . unwrap ( ) ;
1536
1637 let conn = if db_config. db_type == "postgres" {
17- match postgres_connection ( db_config) . await {
18- Ok ( conn) => conn,
19- Err ( e) => {
20- log:: error!( "Failed to connect to postgres: {e}" ) ;
21- log:: info!( "Falling back to sqlite" ) ;
38+ if should_check_port_first ( & db_config. db_url ) {
39+ if !is_port_reachable ( & db_config. db_url ) {
40+ log:: info!( "Local postgres port not reachable, falling back to sqlite" ) ;
2241 sqlite_connection ( db_config)
2342 . await
2443 . expect ( "Cannot connect to any database" )
44+ } else {
45+ match postgres_connection ( db_config) . await {
46+ Ok ( conn) => conn,
47+ Err ( e) => {
48+ log:: error!( "Failed to connect to postgres: {e}" ) ;
49+ log:: info!( "Falling back to sqlite" ) ;
50+ sqlite_connection ( db_config)
51+ . await
52+ . expect ( "Cannot connect to any database" )
53+ }
54+ }
55+ }
56+ } else {
57+ match postgres_connection ( db_config) . await {
58+ Ok ( conn) => conn,
59+ Err ( e) => {
60+ log:: error!( "Failed to connect to postgres: {e}" ) ;
61+ log:: info!( "Falling back to sqlite" ) ;
62+ sqlite_connection ( db_config)
63+ . await
64+ . expect ( "Cannot connect to any database" )
65+ }
2566 }
2667 }
2768 } else {
@@ -34,6 +75,29 @@ pub async fn database_connection(db_config: &DbConfig) -> DatabaseConnection {
3475 conn
3576}
3677
78+ fn should_check_port_first ( db_url : & str ) -> bool {
79+ if let Ok ( url) = Url :: parse ( db_url) {
80+ if let Some ( host) = url. host_str ( ) {
81+ return host == "localhost"
82+ || host == "127.0.0.1"
83+ || host == "::1"
84+ || host == "0.0.0.0" ;
85+ }
86+ }
87+ false
88+ }
89+
90+ fn is_port_reachable ( db_url : & str ) -> bool {
91+ if let Ok ( url) = Url :: parse ( db_url) {
92+ if let ( Some ( host) , Some ( port) ) = ( url. host_str ( ) , url. port ( ) ) {
93+ if let Ok ( addr) = format ! ( "{host}:{port}" ) . parse :: < SocketAddr > ( ) {
94+ return TcpStream :: connect_timeout ( & addr, Duration :: from_millis ( 100 ) ) . is_ok ( ) ;
95+ }
96+ }
97+ }
98+ false
99+ }
100+
37101async fn postgres_connection ( db_config : & DbConfig ) -> Result < DatabaseConnection , MegaError > {
38102 let db_url = db_config. db_url . to_owned ( ) ;
39103 log:: info!( "Connecting to database: {db_url}" ) ;
@@ -61,11 +125,59 @@ fn setup_option(db_url: impl Into<String>) -> ConnectOptions {
61125 let mut opt = ConnectOptions :: new ( db_url) ;
62126 opt. max_connections ( 5 )
63127 . min_connections ( 1 )
64- . acquire_timeout ( Duration :: from_secs ( 3 ) )
65- . connect_timeout ( Duration :: from_secs ( 3 ) )
128+ . acquire_timeout ( Duration :: from_secs ( 1 ) )
129+ . connect_timeout ( Duration :: from_secs ( 1 ) )
66130 . idle_timeout ( Duration :: from_secs ( 8 ) )
67131 . max_lifetime ( Duration :: from_secs ( 8 ) )
68132 . sqlx_logging ( true )
69133 . sqlx_logging_level ( log:: LevelFilter :: Debug ) ;
70134 opt
71135}
136+
137+ #[ cfg( test) ]
138+ pub mod test {
139+ use super :: * ;
140+
141+ /// Creates a test database connection for unit tests.
142+ pub fn test_local_db_address ( ) {
143+ assert ! ( "postgres://mono:mono@localhost:5432/mono_test"
144+ . parse:: <Url >( )
145+ . is_ok( ) ) ;
146+
147+ // Test localhost variants - should return true
148+ assert_eq ! (
149+ should_check_port_first( "postgres://mono:mono@localhost:5432/mono_test" ) ,
150+ true
151+ ) ;
152+ assert_eq ! (
153+ should_check_port_first
( "postgres://mono:[email protected] :5432/mono_test" ) , 154+ true
155+ ) ;
156+ assert_eq ! (
157+ should_check_port_first( "postgres://mono:mono@::1:5432/mono_test" ) ,
158+ true
159+ ) ;
160+ assert_eq ! (
161+ should_check_port_first
( "postgres://mono:[email protected] :5432/mono_test" ) , 162+ true
163+ ) ;
164+
165+ // Test remote addresses - should return false
166+ assert_eq ! (
167+ should_check_port_first
( "postgres://mono:[email protected] :5432/mono_test" ) , 168+ false
169+ ) ;
170+ assert_eq ! (
171+ should_check_port_first
( "postgres://mono:[email protected] :5432/mono_test" ) , 172+ false
173+ ) ;
174+ assert_eq ! (
175+ should_check_port_first
( "postgres://mono:[email protected] :5432/mono_test" ) , 176+ false
177+ ) ;
178+
179+ // Test invalid URLs - should return false
180+ assert_eq ! ( should_check_port_first( "invalid_url" ) , false ) ;
181+ assert_eq ! ( should_check_port_first( "" ) , false ) ;
182+ }
183+ }
0 commit comments