fix(ssh): prevent duplicate SSH tunnels from concurrent pool creation (#891)

start_tunnel() and start_chain() had a check-then-act race condition:
multiple concurrent callers could all see an empty cache and each
spawn a new SSH tunnel. The tunnel that lost the insert race was
orphaned, causing queries routed through it to fail.

Use double-check locking with stale entry eviction: hold the lock for
the cache lookup, release it during the slow SSH handshake, then
re-check under the lock before inserting. If another caller already
created the tunnel, abort the duplicate and return the existing port.

Also check handle liveness on cache hits so that callers don't get a
dead port back when the background tunnel task has exited.

Add a matching re-check in the MongoDB connection pool creation path.

Add idle_timeout_secs connection option (default 60s) to preempt
server-side connection idle timeouts that cause "unexpected end of
file" errors on pooled MongoDB connections.

Reject MongoDB queries in the generic SQL execution path before any
pool or session-key creation. Previously, a typo in a MongoDB shell
command would fall through to executeMulti / execute_sql_statement,
which called get_or_create_pool_for_session and leaked a session-scoped
MongoDB Client (and SSH tunnel resources) before eventually returning
"Use MongoDB-specific commands".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
James-LeongandClaude Opus 4.7 authored and t8y2 committed 2026-06-09 10:29:42 +08:00
1 parent 32164d5aea
commit 171c7cd8d7
15 files changed
+111 -16

No files matched your search

+4 -2
View File
@@ -247,6 +247,7 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
let url = connection_url_for_endpoint(&config, &host, port);
let target = redacted_connection_url_for_endpoint(&config, &host, port);
let connect_timeout = std::time::Duration::from_secs(config.effective_connect_timeout_secs());
let idle_timeout = std::time::Duration::from_secs(config.idle_timeout_secs);
log::info!("[test_connection] db_type={:?} target={}", config.db_type, target);
let result = match probe_result {
Err(e) => Err(e),
@@ -324,7 +325,7 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
}
}
DatabaseType::MongoDb => {
let native_err = match db::mongo_driver::connect(&url, connect_timeout).await {
let native_err = match db::mongo_driver::connect(&url, connect_timeout, idle_timeout).await {
Ok(client) => {
match db::mongo_driver::test_connection(&client, connect_timeout, config.effective_database())
.await
@@ -434,6 +435,7 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
probe_connection_endpoint(&db_config, &host, port).await?;
let url = connection_url_for_endpoint(&db_config, &host, port);
let connect_timeout = std::time::Duration::from_secs(db_config.effective_connect_timeout_secs());
let idle_timeout = std::time::Duration::from_secs(db_config.idle_timeout_secs);
let pool = match db_config.db_type {
DatabaseType::Mysql => {
@@ -489,7 +491,7 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
PoolKind::DuckDb(con)
}
DatabaseType::MongoDb => {
let native_err = match db::mongo_driver::connect(&url, connect_timeout).await {
let native_err = match db::mongo_driver::connect(&url, connect_timeout, idle_timeout).await {
Ok(client) => {
match db::mongo_driver::test_connection(&client, connect_timeout, db_config.effective_database())
.await