归还、Drop 与维护任务
主线位置:连接池主线的后半段。
fetch_one(&pool)返回之前,acquire借来的PoolConnection被 drop;它的Drop把真正的归还交给一个后台任务,调用方不等。本页讲这个任务做了什么、按什么顺序做,连接还有哪几种离开池的方式,以及另一个后台任务怎么按idle_timeout/max_lifetime回收连接、补足min_connections,最后是Pool::close。
flowchart TB
dropc["连接被 drop"] --> cod{close_on_drop}
cod -->|是| tac["后台关闭"]
cod -->|否| rtp["spawn 归还"]
rtp --> isc{池已关闭}
isc -->|是| closeit["close 连接"]
isc -->|否| life{"超过寿命"}
life -->|是| closeit
life -->|否| ar["after_release"]
ar -->|false 或出错| closeit
ar -->|true| ping["ping 一个来回"]
ping -->|失败| closeit
ping -->|成功| rel["入队再放许可"]
closeit --> minc["补足最小连接"]
tac --> minc
三种形态:Live、Idle、Floating
connection.rs 开头的四个类型:
pub struct PoolConnection<DB: Database> {
live: Option<Live<DB>>,
close_on_drop: bool,
pub(crate) pool: Arc<PoolInner<DB>>,
}
pub(super) struct Live<DB: Database> {
pub(super) raw: DB::Connection,
pub(super) created_at: Instant,
}
pub(super) struct Idle<DB: Database> {
pub(super) live: Live<DB>,
pub(super) idle_since: Instant,
}
/// RAII wrapper for connections being handled by functions that may drop them
pub(super) struct Floating<DB: Database, C> {
pub(super) inner: C,
pub(super) guard: DecrementSizeGuard<DB>,
}
| 形态 | 在哪里 | 带的时间戳 | 谁负责 size 和许可 |
|---|---|---|---|
Idle | 空闲队列 idle_conns 里 | created_at、idle_since | 没人:空闲连接只算在 size 里,不占许可 |
Floating<Idle> / Floating<Live> | 在池和用户之间转手:acquire 里、归还任务里、维护任务里 | 同上 | DecrementSizeGuard,drop 时 size 减一、放一个许可 |
PoolConnection(内含 Live) | 用户手里 | created_at | 隐式持有:reattach 时 guard 被 cancel 掉,计数和许可留在原处 |
max_lifetime 比的是 created_at,idle_timeout 比的是 idle_since。Live::into_idle 在每次入队时把 idle_since 设成当前时刻。
PoolConnection 自己不带 guard。它被 drop 时用 Live::float 重新造一个(DecrementSizeGuard::new_permit,注释是「create a new guard from a previously leaked permit」),把隐式持有的那份计数和许可重新交给 RAII 对象。
Drop 只 spawn 一个任务
impl<DB: Database> Drop for PoolConnection<DB> {
fn drop(&mut self) {
if self.close_on_drop {
crate::rt::spawn(self.take_and_close());
return;
}
// We still need to spawn a task to maintain `min_connections`.
if self.live.is_some() || self.pool.options.min_connections > 0 {
crate::rt::spawn(self.return_to_pool());
}
}
}
Drop 是同步的,而归还要做 I/O(至少一个 Sync 来回),所以只能交给一个新任务。live 已经被 detach / leak / close 取走时,只有设了 min_connections 才需要这个任务,用来补连接。
PoolConnection::return_to_pool 在返回 future 之前就把连接 float 好:
pub fn return_to_pool(&mut self) -> impl Future<Output = ()> + Send + 'static {
// float the connection in the pool before we move into the task
// in case the returned `Future` isn't executed, like if it's spawned into a dying runtime
// https://github.com/launchbadge/sqlx/issues/1396
// Type hints seem to be broken by `Option` combinators in IntelliJ Rust right now (6/22).
let floating: Option<Floating<DB, Live<DB>>> =
self.live.take().map(|live| live.float(self.pool.clone()));
let pool = self.pool.clone();
async move {
let returned_to_pool = if let Some(floating) = floating {
floating.return_to_pool().await
} else {
false
};
if !returned_to_pool {
pool.min_connections_maintenance(None).await;
}
}
}
guard 被 move 进了 future。spawn 出去的任务如果永远不跑(运行时正在关闭),future 被 drop,guard 跟着 drop,计数和许可照样还回去。这是 #1799(0.5.12)修的问题,它的标题是「don't leak connections if drop task doesn't run」,issue #1396 的现象是在自己起的线程里用池,几次之后 pool.begin 永远卡住。rt::spawn 找不到 tokio 运行时会 panic,所以 PoolConnection 只能在运行时里面 drop。
归还是异步的,在 current_thread 运行时上看得最清楚:任务不让出,归还任务就一步也跑不了(src/bin/current_thread.rs 的前半段):
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
// ...
let pool = PgPoolOptions::new()
.max_connections(1)
.connect_with(lab::opts("pool-ct"))
.await?;
println!("--- drop, then try_acquire right away ---");
let conn = pool.acquire().await?;
drop(conn);
println!("try_acquire: {:?}", pool.try_acquire().map(|_| ()));
println!("size={} num_idle={}", pool.size(), pool.num_idle());
println!("--- drop, then acquire().await ---");
let started = Instant::now();
let conn = pool.acquire().await?;
println!("acquire took {:?}", started.elapsed());
drop(conn);
// ...
}
--- drop, then try_acquire right away ---
try_acquire: None
size=1 num_idle=0
--- drop, then acquire().await ---
acquire took 593.625µs
...
刚 drop 完,连接既不在空闲队列里,许可也没放出来,try_acquire 得到 None。紧接着的 acquire().await 在信号量上挂起,这时归还任务才开始跑:ping 一个来回、推回队列、放许可,acquire 被叫醒,前后约 0.6 毫秒。acquire 循环里 yield_now 那一段也是为这种「必须让出,归还任务才能前进」的情形写的,见获取连接。
return_to_pool 的顺序
Floating<Live>::return_to_pool:
async fn return_to_pool(mut self) -> bool {
// Immediately close the connection.
if self.guard.pool.is_closed() {
self.close().await;
return false;
}
// If the connection is beyond max lifetime, close the connection and
// immediately create a new connection
if is_beyond_max_lifetime(&self.inner, &self.guard.pool.options) {
self.close().await;
return false;
}
if let Some(test) = &self.guard.pool.options.after_release {
// ... Ok(true) 继续;Ok(false) 正常 close;Err 打 warn 后 close_hard
}
// test the connection on-release to ensure it is still viable,
// and flush anything time-sensitive like transaction rollbacks
// if an Executor future/stream is dropped during an `.await` call, the connection
// is likely to be left in an inconsistent state, in which case it should not be
// returned to the pool; also of course, if it was dropped due to an error
// this is simply a band-aid as SQLx-next connections should be able
// to recover from cancellations
if let Err(error) = self.raw.ping().await {
tracing::warn!(
%error,
"error occurred while testing the connection on-release",
);
// Connection is broken, don't try to gracefully close.
self.close_hard().await;
false
} else {
// if the connection is still viable, release it to the pool
self.release();
true
}
}
- 池已关闭:直接关掉(发
Terminate),不再入队。Pool::close靠这一步等借出的连接一条条关掉。 - 超过
max_lifetime:关掉。这是max_lifetime被检查的两个地方之一,另一个是维护任务;借出时不查。 after_release:用户的最后一道关。放在 ping 之前,回调拿到的连接上可能还有没读完的东西,它跑查询时wait_until_ready会先把它们读掉。- ping:无条件,没有开关。Postgres 的
ping写一个Sync后调wait_until_ready:先 flush 写缓冲区里攒着的东西,再把所有欠着的ReadyForQuery读完。注释点了它的两个目的:一是查询的 future 在中途被 drop 后,连接上还有半截结果,读不完就说明连接坏了,不能放回去;二是「flush anything time-sensitive like transaction rollbacks」,Transaction被 drop 时只是把ROLLBACK排进写缓冲区,要靠这次 flush 才真正发出去,见事务。 release:放回空闲队列。
前两步不碰网络,先做;after_release 可能要跑查询,放在 ping 前面,这样 ping 是交回队列前最后一次和服务端对账。任一步决定关掉连接,return_to_pool 返回 false,外层接着调 min_connections_maintenance 补连接。
release:先入队,再放许可
pub(super) fn release(&self, floating: Floating<DB, Live<DB>>) {
// `options.after_release` and other checks are in `PoolConnection::return_to_pool()`.
let Floating { inner: idle, guard } = floating.into_idle();
if self.idle_conns.push(idle).is_err() {
panic!("BUG: connection queue overflow in release()");
}
// NOTE: we need to make sure we drop the permit *after* we push to the idle queue
// don't decrease the size
guard.release_permit();
self.num_idle.fetch_add(1, Ordering::AcqRel);
}
release_permit 放一个许可,然后 cancel 掉 guard,size 不减:连接还在池里,只是从「借出」变成「空闲」。
顺序反过来会怎样:许可一放出来,排在信号量最前面的 acquire 就被叫醒,它下一步是 pop_idle。如果这时连接还没推进队列,它弹不到东西,转去 try_increment_size:池没满就多建一条连接,池满了就落到「woke but was unable to acquire idle connection or open new one」那条路径空转一圈。先入队后放许可,被叫醒的等待者一定能弹到这条连接。
num_idle 在最后才加一,所以它是个近似值:有一小段时间连接已经在队列里、可以被借走,num_idle() 还没算上它。push 失败直接 panic,因为空闲队列容量是 max_connections,空闲连接数不可能超过它,失败只能是计数出了 bug。
主动离开池:close、close_on_drop、detach、leak
| 方法 | 连接 | size | 许可 | 之后 |
|---|---|---|---|---|
close().await | 当场发 Terminate 关掉 | 关完减一 | 关完才放 | 调用方等关闭完成 |
close_on_drop() | drop 时在后台任务里关,最多等 CLOSE_ON_DROP_TIMEOUT(5 秒) | 关完减一 | 关完才放 | 然后补 min_connections |
detach() | 交给调用方,变成普通的 PgConnection | 立刻减一 | 立刻放 | 池可以再开一条,总数可以超过 max_connections |
leak() | 交给调用方 | 不变 | 永远不放 | 池的容量永久少一 |
close() 的文档说它等于先 detach 再 close,「but the connection permit is retained for the duration so that the pool may not exceed max_connections」。detach 走 Floating::detach,返回 inner.raw 时 guard 被 drop;leak 只是 self.take_live().raw,没有造 guard,PoolConnection 隐式持有的那份计数和许可就再也没人还了。
max_connections(1)、acquire_timeout 500 毫秒的池,经代理连库,各试一次(src/bin/detach.rs):
println!("--- close_on_drop ---");
p.quiet.store(true, Ordering::SeqCst);
let pl = pool(p.port, "pool-cod").await?;
let mut conn = pl.acquire().await?;
p.quiet.store(false, Ordering::SeqCst);
conn.close_on_drop();
drop(conn);
println!("dropped: size={}", pl.size());
settle().await;
println!("later: size={} backends={}", pl.size(), lab::backends(&mut obs, "pool-cod").await.len());
p.quiet.store(true, Ordering::SeqCst);
pl.close().await;
println!("--- detach ---");
let pl = pool(p.port, "pool-detach").await?;
let conn = pl.acquire().await?;
let mut raw = conn.detach();
println!("detached: size={} num_idle={}", pl.size(), pl.num_idle());
let other = pl.acquire().await?;
println!("acquired another: size={} backends={}", pl.size(), lab::backends(&mut obs, "pool-detach").await.len());
let one: i32 = sqlx::query_scalar("select 1").fetch_one(&mut raw).await?;
println!("detached connection still works: {one}");
drop(other);
raw.close().await?;
pl.close().await;
println!("--- leak ---");
let pl = pool(p.port, "pool-leak").await?;
let conn = pl.acquire().await?;
let raw = conn.leak();
settle().await;
println!("leaked: size={} num_idle={}", pl.size(), pl.num_idle());
println!("acquire: {:?}", pl.acquire().await.err());
let closed = tokio::time::timeout(Duration::from_secs(1), pl.close()).await;
println!("pool.close() within 1s: {}", if closed.is_ok() { "done" } else { "still waiting" });
raw.close().await?;
--- close_on_drop ---
dropped: size=1
[conn 1] C->S Terminate
later: size=0 backends=0
--- detach ---
detached: size=0 num_idle=0
acquired another: size=1 backends=2
detached connection still works: 1
--- leak ---
leaked: size=1 num_idle=0
acquire: Some(PoolTimedOut)
pool.close() within 1s: still waiting
close_on_drop:drop 之后没有 ping,只有一条Terminate;size在后台任务关完之后才变成 0。detach:size立刻是 0,于是一个max_connections(1)的池在服务端有了两条连接(backends=2)。leak:size停在 1,唯一的许可被永久占用,acquire超时;pool.close()要等所有许可回来,所以永远等不完。
维护任务
spawn_maintenance_tasks 在 new_arc 里调用,每个池一个:
fn spawn_maintenance_tasks<DB: Database>(pool: &Arc<PoolInner<DB>>) {
// NOTE: use `pool_weak` for the maintenance tasks
// so they don't keep `PoolInner` from being dropped.
let pool_weak = Arc::downgrade(pool);
let period = match (pool.options.max_lifetime, pool.options.idle_timeout) {
(Some(it), None) | (None, Some(it)) => it,
(Some(a), Some(b)) => cmp::min(a, b),
(None, None) => {
if pool.options.min_connections > 0 {
crate::rt::spawn(async move {
if let Some(pool) = pool_weak.upgrade() {
pool.min_connections_maintenance(None).await;
}
});
}
return;
}
};
// Immediately cancel this task if the pool is closed.
let mut close_event = pool.close_event();
crate::rt::spawn(async move {
let _ = close_event
.do_until(async {
// If the last handle to the pool was dropped while we were sleeping
while let Some(pool) = pool_weak.upgrade() {
if pool.is_closed() {
return;
}
let next_run = Instant::now() + period;
// Go over all idle connections, check for idleness and lifetime,
// and if we have fewer than min_connections after reaping a connection,
// open a new one immediately. Note that other connections may be popped from
// the queue in the meantime - that's fine, there is no harm in checking more
for _ in 0..pool.num_idle() {
if let Some(conn) = pool.try_acquire() {
if is_beyond_idle_timeout(&conn, &pool.options)
|| is_beyond_max_lifetime(&conn, &pool.options)
{
let _ = conn.close().await;
pool.min_connections_maintenance(Some(next_run)).await;
} else {
pool.release(conn.into_live());
}
}
}
// Don't hold a reference to the pool while sleeping.
drop(pool);
// ... sleep 到 next_run;已经过了就 yield_now
}
})
.await;
});
}
- 周期是
idle_timeout和max_lifetime中较小的那个,默认是 10 分钟。两个都是None时没有周期任务,只在设了min_connections时 spawn 一次性的补连接任务。 - 第一轮在建池时立刻跑,之后每隔一个周期跑一轮。检查发生在这些时刻,所以一条连接被回收的时间是「超时之后的下一轮」,最多晚一个周期。
- 每轮用
try_acquire把空闲连接一条条取出来看,取的时候占一个许可(借出的连接不受影响,也不会被检查)。超时的用close正常关掉(发Terminate),然后min_connections_maintenance补连接;没超时的release放回队尾。因为是放回队尾,转一圈正好把每条空闲连接看一遍。 - 任务只持有
Weak,睡觉前把升级出来的Arc扔掉;池的最后一个句柄 drop 后,下一轮upgrade失败,任务结束。池关闭时do_until让它立刻结束。
idle_timeout:实验
idle_timeout(1s)、max_lifetime(None),周期就是 1 秒。同时借三条再还回去,每 250 毫秒看一次池和 pg_stat_activity,min_connections 分别为 0 和 1(src/bin/reap.rs):
let pool = PgPoolOptions::new()
.min_connections(min)
.idle_timeout(Duration::from_secs(1))
.max_lifetime(None)
.connect_with(lab::opts(app))
.await?;
// check out three at once so the pool has to open three
let (a, b, c) = (pool.acquire().await?, pool.acquire().await?, pool.acquire().await?);
drop((a, b, c));
tokio::time::sleep(Duration::from_millis(50)).await;
for _ in 0..(if min == 0 { 10 } else { 18 }) {
println!(
"{:>5}ms size={} idle={} pids={:?}",
clock.ms(),
pool.size(),
pool.num_idle(),
lab::backends(&mut obs, app).await
);
tokio::time::sleep(Duration::from_millis(250)).await;
}
--- idle_timeout(1s), max_lifetime(None), min_connections(0) ---
136ms size=3 idle=3 pids=[328, 329, 330]
398ms size=3 idle=3 pids=[328, 329, 330]
653ms size=3 idle=3 pids=[328, 329, 330]
908ms size=3 idle=3 pids=[328, 329, 330]
1164ms size=3 idle=3 pids=[328, 329, 330]
1420ms size=3 idle=3 pids=[328, 329, 330]
1674ms size=3 idle=3 pids=[328, 329, 330]
1929ms size=3 idle=3 pids=[328, 329, 330]
2183ms size=0 idle=0 pids=[]
2437ms size=0 idle=0 pids=[]
--- idle_timeout(1s), max_lifetime(None), min_connections(1) ---
146ms size=3 idle=3 pids=[334, 335, 336]
406ms size=3 idle=3 pids=[334, 335, 336]
659ms size=3 idle=3 pids=[334, 335, 336]
913ms size=3 idle=3 pids=[334, 335, 336]
1167ms size=3 idle=3 pids=[334, 335, 336]
1420ms size=3 idle=3 pids=[334, 335, 336]
1674ms size=3 idle=3 pids=[334, 335, 336]
1930ms size=3 idle=3 pids=[334, 335, 336]
2182ms size=1 idle=1 pids=[337]
2437ms size=1 idle=1 pids=[337]
2691ms size=1 idle=1 pids=[337]
2945ms size=1 idle=1 pids=[337]
3200ms size=1 idle=1 pids=[337]
3455ms size=1 idle=1 pids=[337]
3710ms size=1 idle=1 pids=[337]
3965ms size=1 idle=1 pids=[337]
4220ms size=1 idle=1 pids=[338]
4475ms size=1 idle=1 pids=[338]
- 连接在约 0.1 秒时归还。第 1 秒那一轮它们只空闲了 0.9 秒,留下;第 2 秒那一轮超过了 1 秒,三条一起关掉。实际空闲了约 2 秒才被回收,正是「超时后的下一轮」。
min_connections(1)时三条都被关掉,然后补了一条新的(pid 337),而不是留下一条旧的:维护任务对每条超时的连接一视同仁,关掉之后才看数量够不够。- 补上的 337 也只是一条空闲连接,2 秒后同样超时,被换成 338。设了
min_connections又设了idle_timeout,池在没有流量时也会每隔一两个周期重建这几条连接。
max_lifetime:借出时不查
max_lifetime(2s)、idle_timeout(None),周期 2 秒,维护任务在约 0、2、4、6 秒各跑一轮。建池时那条连接在第 2 秒那轮只活了不到 2 秒,留下;第 2.6 秒把它借出来(src/bin/lifetime.rs):
let pool = PgPoolOptions::new()
.max_lifetime(Duration::from_secs(2))
.idle_timeout(None)
.connect_with(lab::opts(APP))
.await?;
println!("{:>5}ms built: size={} pids={:?}", clock.ms(), pool.size(), lab::backends(&mut obs, APP).await);
tokio::time::sleep(Duration::from_millis(2600)).await;
let mut conn = pool.acquire().await?;
let pid: i32 = sqlx::query_scalar("select pg_backend_pid()").fetch_one(&mut *conn).await?;
println!("{:>5}ms acquired, pid={pid}", clock.ms());
drop(conn);
tokio::time::sleep(Duration::from_millis(50)).await;
println!("{:>5}ms after drop: size={} idle={} pids={:?}", clock.ms(), pool.size(), pool.num_idle(), lab::backends(&mut obs, APP).await);
// a fresh connection that just sits in the idle queue
drop(pool.acquire().await?);
tokio::time::sleep(Duration::from_millis(50)).await;
for _ in 0..8 {
println!("{:>5}ms size={} idle={} pids={:?}", clock.ms(), pool.size(), pool.num_idle(), lab::backends(&mut obs, APP).await);
tokio::time::sleep(Duration::from_millis(500)).await;
}
27ms built: size=1 pids=[353]
2639ms acquired, pid=353
2692ms after drop: size=0 idle=0 pids=[]
2773ms size=1 idle=1 pids=[356]
3278ms size=1 idle=1 pids=[356]
3782ms size=1 idle=1 pids=[356]
4287ms size=1 idle=1 pids=[356]
4793ms size=1 idle=1 pids=[356]
5296ms size=1 idle=1 pids=[356]
5800ms size=1 idle=1 pids=[356]
6304ms size=0 idle=0 pids=[]
- 2.6 秒时借到的还是 353,它已经活了 2.6 秒,超过了
max_lifetime:check_idle_conn不看寿命。 - 用完一还,
return_to_pool查到超龄,关掉,size变成 0。这是寿命被检查的第一个地方。 - 新建的 356 在 2.7 秒左右入队。第 4 秒那轮它活了 1.3 秒,留下;第 6 秒那轮超过 2 秒,被维护任务关掉。这是第二个地方。
max_lifetime 原来是在借出时查的0.7.3 的 check_idle_conn 开头就是「If the connection we pulled has expired, close the connection and immediately create a new connection」,超龄的连接在 acquire 里当场关掉、重连。#3065(首次发布于 0.7.4)把它挪到了 return_to_pool,并重写了维护任务。它实现的 issue #3051 描述的是延迟尖刺:连接差不多同时建立,到了 max_lifetime 就在查询的路径上集体重连,而旧的维护任务「only looks at (size - min_size) connections」,size 等于 min_connections 时什么都不做。PR 的两点改动是「Move max lifetime checking from acquire to release. This reduces the number of query-time reconnections due to max lifetime」,以及让维护任务逐条检查空闲连接、关掉一条就立刻补一条。PR 作者还留了一个问题:「whether we should be replacing connection below min_connections due to being idle - this PR does that」,上面 min_connections(1) 的实验里 337 被换成 338,就是这个选择的结果。
min_connections 只在回收之后补
补连接的函数是 try_min_connections:size 不到 min_connections 就 try_acquire 一个许可(不排队,拿不到就算了)、try_increment_size、connect、release。它不经过 after_release(注释说连接「was never provided to user code besides after_connect」)。外面包一层 min_connections_maintenance,截止时间默认 300 秒,失败只打 debug 日志。
调用它的地方只有这几处:
| 调用者 | 什么时候 |
|---|---|
connect_with | 建池时,先补足再返回 |
| 周期维护任务 | 关掉了一条超时连接之后 |
(None, None) 分支的一次性任务 | 建池时,只跑一次 |
PoolConnection 的归还任务 | 连接没能放回队列(关掉了)之后 |
close_on_drop 的关闭任务 | 关完之后 |
周期任务的循环体只在「关掉了一条」时补,队列里一条连接都没有时,for _ in 0..pool.num_idle() 一次都不执行。下面四种池都设了 min_connections(3),每种建好后在 0、500、2000 毫秒各看一次(src/bin/minconn.rs):
let pool = PgPoolOptions::new()
.min_connections(3)
.connect_with(lab::opts("pool-min-a"))
.await?;
watch("connect ", &pool, "pool-min-a").await;
pool.close().await;
let pool = PgPoolOptions::new()
.min_connections(3)
.connect_lazy_with(lab::opts("pool-min-b"));
watch("connect_lazy (default)", &pool, "pool-min-b").await;
pool.close().await;
let pool = PgPoolOptions::new()
.min_connections(3)
.idle_timeout(None)
.max_lifetime(None)
.connect_lazy_with(lab::opts("pool-min-c"));
watch("connect_lazy (no reap)", &pool, "pool-min-c").await;
pool.close().await;
// lazy pool with default timeouts: does using it once fill the rest?
let pool = PgPoolOptions::new()
.min_connections(3)
.connect_lazy_with(lab::opts("pool-min-d"));
drop(pool.acquire().await?);
watch("lazy + one acquire ", &pool, "pool-min-d").await;
pool.close().await;
watch 打印 size、num_idle 和 pg_stat_activity 里属于这个池的后端数。
connect_lazy,idle_timeout 和 max_lifetime 都是默认值)在 2 秒时的 size 是多少?第四种呢?平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。答案在页末。Pool::close
Pool::close 转给 PoolInner::close:
pub(super) fn close(self: &Arc<Self>) -> impl Future<Output = ()> + '_ {
self.mark_closed();
async move {
// For child pools, we need to acquire permits we actually have rather than
// max_connections
let permits_to_acquire = if self.options.parent_pool.is_some() {
// Child pools start with 0 permits, so we acquire based on current size
self.size()
} else {
// Parent pools can acquire all max_connections permits
self.options.max_connections
};
let _permits = self.semaphore.acquire(permits_to_acquire).await;
while let Some(idle) = self.idle_conns.pop() {
let _ = idle.live.raw.close().await;
}
self.num_idle.store(0, Ordering::Release);
self.size.store(0, Ordering::Release);
}
}
mark_closed 在返回 future 之前执行:只调用不 await,池也已经关了,排队的和之后的 acquire 立刻得到 PoolClosed。await 这个 future 是在等收尾:一次要齐全部 max_connections 个许可,意思是「所有借出的连接都回来了」;借出的连接归还时 return_to_pool 看到池已关闭,直接关掉,关完才放许可。许可齐了再把空闲队列里的连接逐条 close。
两条连接,一条空闲、一条被一个任务拿着、300 毫秒后才放手,第 0 毫秒调 close,第 100 毫秒另一个任务来 acquire(src/bin/close.rs):
let pool = PgPoolOptions::new()
.max_connections(2)
.connect_with(lab::via_proxy("pool-close", p.port))
.await?;
let held = pool.acquire().await?; // conn 1
drop(pool.acquire().await?); // conn 2, back in the idle queue
tokio::time::sleep(Duration::from_millis(100)).await;
let clock = Arc::new(lab::Clock::new());
println!("{:>4}ms size={} idle={}", clock.ms(), pool.size(), pool.num_idle());
p.quiet.store(false, Ordering::SeqCst);
let c = clock.clone();
let holder = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(300)).await;
println!("{:>4}ms holder drops its connection", c.ms());
drop(held);
});
let (pool2, c) = (pool.clone(), clock.clone());
let late = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
println!("{:>4}ms acquire after close: {:?}", c.ms(), pool2.acquire().await.err());
});
println!("{:>4}ms calling close()", clock.ms());
let closing = pool.close();
println!("{:>4}ms is_closed={} (close future not polled yet)", clock.ms(), pool.is_closed());
closing.await;
println!("{:>4}ms close() returned: size={} idle={}", clock.ms(), pool.size(), pool.num_idle());
0ms size=2 idle=1
0ms calling close()
0ms is_closed=true (close future not polled yet)
103ms acquire after close: Some(PoolClosed)
302ms holder drops its connection
303ms close() returned: size=0 idle=0
[conn 1] C->S Terminate
[conn 2] C->S Terminate
代理按连接编号:conn 1 是建池时那条,被 held 借走;conn 2 是第二次 acquire 新建、随即归还到空闲队列的那条。最后两行是代理在后台任务里打印的,打印时刻比实际发送稍晚,排在了 close() returned 后面。
close().await 返回时,连接可能还没关完issue #3217(标题「Pool::close does not always wait for all connections to close」)在 0.7.4 上报告了两个问题:旧实现按 1..=max_connections 循环,每一轮先清空空闲队列再等许可,最后一轮之后归还的连接会留在队列里,「any connections still in idle_conns are not closed until the pool itself has been dropped」;清空队列时又把空闲连接临时当成借出的连接,凭空造出一个许可,许可数比初始还多,最后一次 acquire 提前完成。报告者是在 SQLite 的 WAL 文件没被删掉时发现的。#3952(首次发布于 0.9.0)改成上面的写法:一次要齐所有许可,再清队列,直接调 raw.close() 不再经过 float;子池只要它当前 size 那么多个许可,因为子池的信号量从 0 开始。
上面实验里 close() 在持有者放手后 1 毫秒返回。按代码,这时两条连接都已经发出了 Terminate:借出的 conn 1 由归还任务关掉,关完才放许可;空闲的 conn 2 在许可齐了之后由 close 自己关掉,代理上的先后也是 conn 1、conn 2。所以 #3952 之后,close().await 返回就意味着池的所有连接都已关闭。
只要有一条连接不回来,close().await 就不会返回:上一节 leak 的那条就是这样。子池也有同样的效果:子池从父池偷来的许可攒在子池自己的信号量里,要等子池的 PoolInner 被 drop 才还给父池,在那之前父池的 close() 凑不齐许可。src/bin/current_thread.rs 末尾:
child.close().await;
println!("child closed; parent permits still lent to the child: parent.close() within 1s: {}",
if tokio::time::timeout(std::time::Duration::from_secs(1), parent.close()).await.is_ok() { "done" } else { "still waiting" });
drop(child);
parent.close().await;
println!("after drop(child): parent closed");
...
child closed; parent permits still lent to the child: parent.close() within 1s: still waiting
after drop(child): parent closed
Pool::close 的文档说,不等这个 future 完成也可以,剩下的连接会在池的最后一个句柄被 drop 时被 drop,但「could happen in a task spawned by Pool internally and so may be unpredictable」;它还特别提醒每次新建池的测试最好等 close 完成,否则即使单线程跑测试也可能撞上服务端的连接数上限。
动脑筋答案
connect t= 0ms: size=3 num_idle=3 backends=3
connect t= 500ms: size=3 num_idle=3 backends=3
connect t=2000ms: size=3 num_idle=3 backends=3
connect_lazy (default) t= 0ms: size=0 num_idle=0 backends=0
connect_lazy (default) t= 500ms: size=0 num_idle=0 backends=0
connect_lazy (default) t=2000ms: size=0 num_idle=0 backends=0
connect_lazy (no reap) t= 0ms: size=1 num_idle=0 backends=1
connect_lazy (no reap) t= 500ms: size=3 num_idle=3 backends=3
connect_lazy (no reap) t=2000ms: size=3 num_idle=3 backends=3
lazy + one acquire t= 0ms: size=1 num_idle=1 backends=1
lazy + one acquire t= 500ms: size=1 num_idle=1 backends=1
lazy + one acquire t=2000ms: size=1 num_idle=1 backends=1
第二种是 0,第四种是 1。
connect:connect_with自己调try_min_connections,返回时已经是 3 条。connect_lazy,默认超时:周期是 10 分钟的维护任务在建池时跑了第一轮,队列是空的,循环体一次都没执行;下一轮在 10 分钟后,而且只有关掉了一条超时连接才会补。connect_lazy的文档和注释说后台任务会补足min_connections,在这种配置下不成立。0.7.3 的周期任务每轮都无条件调一次min_connections_maintenance,#3065(0.7.4)改成只在关掉一条之后调;0.8.6 的这段代码和 0.9.0 相同。connect_lazy,两个超时都是None:走(None, None)分支,一次性任务补连接。第 0 毫秒那次size=1、backends=1是正在建第一条:size已经加上,连接还没进队列(num_idle=0)。connect_lazy后借一次:acquire建了一条,归还成功(returned_to_pool为true),归还任务不调补连接;于是一直是 1 条。要靠connect_lazy预热,就得自己并发借够min_connections条再还回去,或者改用connect。
小结
PoolConnection的Drop只 spawn:开了close_on_drop就在后台关掉,否则return_to_pool。guard 在 spawn 之前就造好,任务不跑也不会漏计数。Drop必须发生在 tokio 运行时里。return_to_pool依次:池已关闭 → 超过max_lifetime→after_release→ 无条件 ping(顺带发出排队的ROLLBACK、读掉残留消息)→release。任何一步决定关掉,就转去补min_connections。release先推回空闲队列再放许可,保证被叫醒的等待者一定弹得到连接;num_idle最后才加,是近似值。detach立刻还计数和许可,总连接数可以超过max_connections;leak永远不还,池容量减一,close()也等不完。- 维护任务的周期是
min(idle_timeout, max_lifetime),默认 10 分钟,建池时先跑一轮;超时连接最晚在下一轮被关掉。max_lifetime只在归还和维护任务里查,借出时不查。 min_connections只在建池(connect)、关掉连接之后补;connect_lazy加默认超时不会预热。close()一调用就拒绝新的acquire;await它等全部许可回来,0.9.0 起返回时所有连接都已关闭。