Skip to content

Commit 0842b70

Browse files
yyk808Copilotgenedna
authored
fix: delay on startup when connecting unreachable local postgres db (#1246)
* fix: delay on startup when connecting unreachable local postgres db * Update common/src/config.rs Co-authored-by: Copilot <[email protected]> Signed-off-by: Neon <[email protected]> * fix clippy --------- Signed-off-by: Neon <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Quanyi Ma <[email protected]>
1 parent 4375701 commit 0842b70

File tree

8 files changed

+165
-46
lines changed

8 files changed

+165
-46
lines changed

common/src/config.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ impl Default for LFSSshConfig {
540540
}
541541
}
542542

543-
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
543+
#[derive(Serialize, Deserialize, Debug, Clone)]
544544
pub struct OauthConfig {
545545
pub github_client_id: String,
546546
pub github_client_secret: String,
@@ -550,6 +550,26 @@ pub struct OauthConfig {
550550
pub allowed_cors_origins: Vec<String>,
551551
}
552552

553+
impl Default for OauthConfig {
554+
fn default() -> Self {
555+
Self {
556+
github_client_id: String::new(),
557+
github_client_secret: String::new(),
558+
ui_domain: "http://localhost".to_string(),
559+
cookie_domain: "localhost".to_string(),
560+
campsite_api_domain: "http://api.gitmono.test:3001".to_string(),
561+
allowed_cors_origins: vec![
562+
"http://localhost",
563+
"http://app.gitmega.com",
564+
"http://app.gitmono.test",
565+
]
566+
.into_iter()
567+
.map(|s| s.to_string())
568+
.collect(),
569+
}
570+
}
571+
}
572+
553573
#[cfg(test)]
554574
mod test {
555575
use super::*;

context/src/lib.rs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use std::sync::Arc;
21
use jupiter::tests::test_storage;
2+
use std::sync::Arc;
33

44
/// This is the main application context for the Mono application.
55
/// It holds shared state and configuration for the application.
@@ -23,19 +23,12 @@ pub struct AppContext {
2323
impl AppContext {
2424
/// Creates a new application context with the given configuration.
2525
pub async fn new(config: common::config::Config) -> Self {
26-
2726
let config = Arc::new(config);
2827

29-
3028
let storage = jupiter::storage::Storage::new(config.clone()).await;
3129

3230
let storage_for_vault = storage.clone();
33-
let vault = tokio::task::spawn_blocking(move || {
34-
vault::integration::vault_core::VaultCore::new(storage_for_vault)
35-
})
36-
.await
37-
.expect("VaultCore::new panicked");
38-
31+
let vault = vault::integration::vault_core::VaultCore::new(storage_for_vault);
3932

4033
#[cfg(feature = "p2p")]
4134
let client = gemini::p2p::client::P2PClient::new(storage.clone(), vault.clone());
@@ -49,31 +42,28 @@ impl AppContext {
4942
#[cfg(feature = "p2p")]
5043
client,
5144
}
52-
53-
5445
}
5546

5647
pub fn wrapped_context(&self) -> Arc<Self> {
5748
Arc::new(self.clone())
5849
}
5950

6051
pub async fn mock(config: common::config::Config) -> Self {
61-
6252
let config = Arc::new(config);
6353

6454
// use Existing test method
6555
let storage = test_storage(config.base_dir.clone()).await;
66-
56+
6757
let storage_for_vault = storage.clone();
6858
let temp_dir = config.base_dir.clone().join("vault");
6959
let key_path = temp_dir.join("core_key.json");
7060
std::fs::create_dir_all(&temp_dir).expect("Mock: Failed to create vault dir");
71-
61+
7262
let vault = tokio::task::spawn_blocking(move || {
7363
vault::integration::vault_core::VaultCore::config(storage_for_vault, key_path)
7464
})
75-
.await
76-
.expect("VaultCore::config panicked");
65+
.await
66+
.expect("VaultCore::config panicked");
7767

7868
#[cfg(feature = "p2p")]
7969
let client = gemini::p2p::client::P2PClient::new(storage.clone(), vault.clone());
@@ -87,4 +77,3 @@ impl AppContext {
8777
}
8878
}
8979
}
90-

jupiter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ aws-sdk-s3 = { workspace = true, features = ["rt-tokio"] }
4141
anyhow = { workspace = true }
4242
tempfile = { workspace = true }
4343
indexmap = { workspace = true }
44+
url = { workspace = true }

jupiter/src/storage/init.rs

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,68 @@
11
use common::config::DbConfig;
22
use common::errors::MegaError;
33
use 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+
};
59
use tracing::log;
10+
use url::Url;
611

712
use crate::migration::apply_migrations;
813

914
use 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.
1334
pub 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+
37101
async 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+
}

mega/src/cli.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ pub fn parse(args: Option<Vec<&str>>) -> MegaResult {
2929
None => cli().try_get_matches().unwrap_or_else(|e| e.exit()),
3030
};
3131

32-
// Get the current directory
32+
// Load configuration from the config file or default location
3333
let current_dir = env::current_dir()?;
34-
// Get the path to the config file in the current directory
34+
let base_dir = common::config::mega_base();
3535
let config_path = current_dir.join("config.toml");
36+
let config_path_alt = base_dir.join("etc/config.toml");
3637

3738
let config = if let Some(path) = matches.get_one::<PathBuf>("config").cloned() {
3839
Config::new(path.to_str().unwrap()).unwrap()
3940
} else if config_path.exists() {
4041
Config::new(config_path.to_str().unwrap()).unwrap()
42+
} else if config_path_alt.exists() {
43+
Config::new(config_path_alt.to_str().unwrap()).unwrap()
4144
} else {
42-
eprintln!("can't find config.toml under {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap());
45+
eprintln!("can't find config.toml under {:?} or {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap(), base_dir);
4346
Config::default()
4447
};
4548

mono/src/cli.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ pub fn parse(args: Option<Vec<&str>>) -> MegaResult {
2929
None => cli().try_get_matches().unwrap_or_else(|e| e.exit()),
3030
};
3131

32-
// Get the current directory
32+
// Load configuration from the config file or default location
3333
let current_dir = env::current_dir()?;
34-
// Get the path to the config file in the current directory
35-
let config_path = current_dir.join("../../config/config.toml");
34+
let base_dir = common::config::mega_base();
35+
let config_path = current_dir.join("config.toml");
36+
let config_path_alt = base_dir.join("etc/config.toml");
3637

3738
let config = if let Some(path) = matches.get_one::<PathBuf>("config").cloned() {
3839
Config::new(path.to_str().unwrap()).unwrap()
3940
} else if config_path.exists() {
4041
Config::new(config_path.to_str().unwrap()).unwrap()
42+
} else if config_path_alt.exists() {
43+
Config::new(config_path_alt.to_str().unwrap()).unwrap()
4144
} else {
42-
eprintln!("can't find config.toml under {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap());
45+
eprintln!("can't find config.toml under {:?} or {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap(), base_dir);
4346
Config::default()
4447
};
4548

mono/src/server/https_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub async fn app(storage: Storage, host: String, port: u16) -> Router {
9494
};
9595

9696
let config = storage.config();
97-
let oauth_config = config.oauth.clone().unwrap();
97+
let oauth_config = config.oauth.clone().unwrap_or_default();
9898
let api_state = MonoApiServiceState {
9999
storage: storage.clone(),
100100
oauth_client: Some(oauth_client(oauth_config.clone()).unwrap()),

vault/src/integration/vault_core.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
use crate::integration::jupiter_backend::JupiterBackend;
32
use common::errors::MegaError;
43
use jupiter::storage::Storage;
@@ -7,7 +6,6 @@ use std::{
76
sync::{Arc, RwLock},
87
};
98

10-
119
use rusty_vault::{
1210
core::Core,
1311
logical::{Operation, Request, Response},
@@ -19,8 +17,6 @@ use tracing::log;
1917

2018
const CORE_KEY_FILE: &str = "core_key.json"; // where the core key is stored, like `root_token`
2119

22-
23-
2420
#[derive(Debug, Clone, Serialize, Deserialize)]
2521
struct CoreKey {
2622
secret_shares: Vec<Vec<u8>>,
@@ -57,7 +53,6 @@ impl VaultCore {
5753
tracing::info!("{key_path:?}");
5854
std::fs::create_dir_all(&dir).expect("Failed to create vault directory");
5955
Self::config(ctx.clone(), key_path)
60-
6156
}
6257

6358
pub fn config(ctx: Storage, key_path: PathBuf) -> Self {
@@ -75,16 +70,13 @@ impl VaultCore {
7570
};
7671
let core = Arc::new(RwLock::new(core));
7772

78-
7973
let key = {
8074
let mut managed_core = core.write().unwrap();
8175
managed_core
8276
.config(core.clone(), None)
8377
.expect("Failed to configure vault core");
8478

8579
let core_key = if !key_path.exists() {
86-
87-
8880
let result = managed_core
8981
.init(&seal_config)
9082
.expect("Failed to initialize vault");
@@ -211,8 +203,7 @@ mod tests {
211203
"Vault core should be initialized"
212204
);
213205
}
214-
215-
206+
216207
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
217208
async fn test_vault_api() {
218209
let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");

0 commit comments

Comments
 (0)