Rust in Everything
基于 sqlx 0.9.0

归还、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 一个任务

PoolConnection 的 Drop:

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
        }
    }
  1. 池已关闭:直接关掉(发 Terminate),不再入队。Pool::close 靠这一步等借出的连接一条条关掉。
  2. 超过 max_lifetime:关掉。这是 max_lifetime 被检查的两个地方之一,另一个是维护任务;借出时不查。
  3. after_release:用户的最后一道关。放在 ping 之前,回调拿到的连接上可能还有没读完的东西,它跑查询时 wait_until_ready 会先把它们读掉。
  4. ping:无条件,没有开关。Postgres 的 ping 写一个 Sync 后调 wait_until_ready:先 flush 写缓冲区里攒着的东西,再把所有欠着的 ReadyForQuery 读完。注释点了它的两个目的:一是查询的 future 在中途被 drop 后,连接上还有半截结果,读不完就说明连接坏了,不能放回去;二是「flush anything time-sensitive like transaction rollbacks」,Transaction 被 drop 时只是把 ROLLBACK 排进写缓冲区,要靠这次 flush 才真正发出去,见事务。
  5. release:放回空闲队列。

前两步不碰网络,先做;after_release 可能要跑查询,放在 ping 前面,这样 ping 是交回队列前最后一次和服务端对账。任一步决定关掉连接,return_to_pool 返回 false,外层接着调 min_connections_maintenance 补连接。

release:先入队,再放许可

PoolInner::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 秒,被维护任务关掉。这是第二个地方。
📝NOTE
冷知识: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 里属于这个池的后端数。

❗IMPORTANT
动脑筋:第二种池(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 后面。

📝NOTE
冷知识:0.9 之前 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 起返回时所有连接都已关闭。

💬 关联讨论

发起讨论
加载中...