Connection Error: pool timed out while waiting for an open connection
主线位置:查询主线的起点
Database::connect。set_schema_search_path不是连接参数,sea-orm 把它拼成一条SET search_path = ...,装进 sqlx 的after_connect,每条新连接执行一次(DatabaseConnection 与 ConnectOptions 的「会话设置」一节)。拼法有三个毛病,本页逐个复现。
flowchart TB
val["set_schema_search_path 的值"] --> q{"以引号开头"}
q -->|是| raw["原样拼在 SET 后面"]
q -->|否| split["按逗号切 每段加引号"]
split --> sql["SET search_path = ..."]
raw --> sql
sql --> ac["after_connect 执行"]
ac -->|语法错误| retry["丢掉连接 退避重连"]
retry --> ac
retry -->|截止时间到| err["pool timed out"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行:sea-orm 2.0.4(sqlx 0.9.0)和 sea-orm 1.1.20(sqlx 0.8.6)。
现象
换了一个 schema 名之后,服务启动时卡住,过了 acquire_timeout(不设时是 30 秒)报:
Connection Error: pool timed out while waiting for an open connection
数据库好好的,用 psql 连得上。错误里没有任何关于 search_path 的字样。
最小复现
src/bin/search_path.rs。先建两个 schema:so_pit3_b 里放一张 t_b,再建一个名字里带双引号的 so_pit3_q"x;每种写法连一次(acquire_timeout 1 秒),打开 sqlx 日志(sqlx=info 的 subscriber),连上后查 current_setting('search_path'):
let mut opt = ConnectOptions::new(DIRECT);
opt
.max_connections(1)
.acquire_timeout(Duration::from_secs(1))
.sqlx_logging(true)
.sqlx_logging_level(log::LevelFilter::Info)
.set_schema_search_path(path);
名字里带引号:
## set_schema_search_path("so_pit3_q\"x")
INFO sqlx::query: summary="SET search_path = \"so_pit3_q\"x\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=282.292µs elapsed_secs=0.000282292
ERROR sqlx_core::pool::inner: error returned from after_connect error=error returned from database: syntax error at or near "x" at line 1244
INFO sqlx::query: summary="SET search_path = \"so_pit3_q\"x\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=272.041µs elapsed_secs=0.000272041
ERROR sqlx_core::pool::inner: error returned from after_connect error=error returned from database: syntax error at or near "x" at line 1244
...
connect 失败,用时 1.00s:Connection Error: pool timed out while waiting for an open connection
拼出来的是 SET search_path = "so_pit3_q"x",语法错误(42601)。这一秒里同样的一对 INFO / ERROR 出现了 7 次:after_connect 失败时 sqlx 记一条 ERROR、扔掉连接、退避重连,直到截止时间(sqlx 卷获取连接)。Database::connect 最后报的只是超时;没有装 tracing subscriber 的程序连那条 ERROR 都看不到。
逗号后面带空格:
## set_schema_search_path("so_pit3, so_pit3_b")
INFO sqlx::query: summary="SET search_path = \"so_pit3\",\" …" db.statement="\n\nSET search_path = \"so_pit3\",\" so_pit3_b\"\n" rows_affected=0 rows_returned=0 elapsed=289.333µs elapsed_secs=0.000289333
search_path = so_pit3, " so_pit3_b"
select v from t_b => Query Error: error returned from database: relation "t_b" does not exist at line 1449
空格被包进了引号,第二个 schema 叫 so_pit3_b(带前导空格),不存在;Postgres 对 search_path 里不存在的 schema 不报错,静默跳过,直到查 t_b 才报 42P01。不带空格的 "so_pit3,so_pit3_b" 能查到 found in so_pit3_b。
以引号开头的值:
## set_schema_search_path("\"so_pit3\"; SET application_name = 'injected'")
INFO sqlx::query: summary="SET search_path = \"so_pit3\"; …" db.statement="\n\nSET search_path = \"so_pit3\"; SET application_name = 'injected'\n" rows_affected=0 rows_returned=0 elapsed=756.208µs elapsed_secs=0.000756208
search_path = so_pit3
select current_setting('application_name') as v => injected
整个值原样拼上,又走简单查询协议,分号后面的语句在每条新连接上都执行。
1.1.20 上五种写法的结果相同,区别只在报错末尾没有 at line N(那是 sqlx 0.9 才加的)。
出处
SqlxPostgresConnector::connect 里拼 SQL 的那段:
let set_search_path_sql = options.schema_search_path.as_ref().map(|schema| {
let mut string = "SET search_path = ".to_owned();
if schema.starts_with('"') {
write!(&mut string, "{schema}").expect("Infallible");
} else {
for (i, schema) in schema.split(',').enumerate() {
if i > 0 {
write!(&mut string, ",").expect("Infallible");
}
if schema.starts_with('"') {
write!(&mut string, "{schema}").expect("Infallible");
} else {
write!(&mut string, "\"{schema}\"").expect("Infallible");
}
}
}
string
});
按逗号切开后不去空格;加引号时不把值里的 " 写成 "";整个值以 " 开头时什么都不做。这条 SQL 随后装进 pool_options.after_connect,用 sqlx::Executor::execute(conn, sqlx::AssertSqlSafe(sql)) 执行,AssertSqlSafe 就是在告诉 sqlx「这条拼出来的 SQL 我担保」。1.1.20 的 connect 是同一段(.unwrap() 代替 .expect,sql.as_str() 代替 AssertSqlSafe)。
1.1.3 之前连引号都不加,名字里有 - 也会这样超时,PR #2436 加了引号、对以引号开头的值保持原样,但没处理值内部的引号(来龙去脉见 database-connection 那一页的冷知识)。在 SeaQL/sea-orm 搜 search_path quote、search_path comma、set_schema_search_path、schema search path space,没有找到报告这三个问题的 issue。
修法
- 普通名字(字母、数字、下划线)直接写,多个之间只用逗号、不加空格:
"so_pit3,so_pit3_b"。 - 需要引号的名字自己把整个值写成合法的 SQL 标识符列表,以
"开头,内部的"写两遍。上面那个 schema 写成"\"so_pit3_q\"\"x\"":
## set_schema_search_path("\"so_pit3_q\"\"x\"")
INFO sqlx::query: summary="SET search_path = \"so_pit3_q\"\"x\"" db.statement="" rows_affected=0 rows_returned=0 elapsed=618.333µs elapsed_secs=0.000618333
search_path = "so_pit3_q""x"
select current_schema() as v => so_pit3_q"x
- 值只能来自配置里写死的常量,不能拼任何外部输入:以引号开头时它就是一段原样执行的 SQL。
怎么确认没中招
Database::connect报pool timed out、而数据库连得上时,装一个sqlx_core=error的 tracing subscriber 再连一次,error returned from after_connect后面就是真正的原因。- 连上之后查一次
select current_setting('search_path'),和配置的值比对:出现" name"这样带空格的引号,就是逗号后的空格。 - 本站没有用
set_schema_search_path。
小结
set_schema_search_path的值被拼进一条SET,在after_connect里执行;拼错时连接一直建不起来,Database::connect只报pool timed out while waiting for an open connection,原因只在 sqlx 的ERROR日志里。- 三个毛病:值里的
"不转义(语法错误、超时);逗号后的空格被包进引号(schema 静默失效);以"开头的值原样拼接(分号后的语句照样执行)。2.0.4 和 1.1.20 相同。 - 普通名字用逗号连、不加空格;特殊名字自己写好整个带引号的列表。