Rust in Everything
基于 sea-orm 2.0.4

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 相同。
  • 普通名字用逗号连、不加空格;特殊名字自己写好整个带引号的列表。

💬 关联讨论

发起讨论
加载中...